Files
GTASource/game/audio/radiostation.cpp
expvintl 419f2e4752 init
2025-02-23 17:40:52 +08:00

6656 lines
232 KiB
C++

//
// audio/radiostation.cpp
//
// Copyright (C) 1999-2007 Rockstar Games. All Rights Reserved.
//
#include "audiodefines.h"
#if NA_RADIO_ENABLED
#include "radiostation.h"
#include "music/musicplayer.h"
#include "northaudioengine.h"
#include "radioaudioentity.h"
#include "radioslot.h"
#include "audioengine/engine.h"
#include "audiosoundtypes/speechsound.h"
#include "audiohardware/device.h"
#include "audiohardware/driver.h"
#include "audiohardware/driverutil.h"
#include "audiohardware/submix.h"
#include "audiosynth/synthcore.h"
#include "grcore/debugdraw.h"
#include "game/clock.h"
#include "game/weather.h"
#include "parser/manager.h"
#include "parsercore/attribute.h"
#include "string/stringhash.h"
#include "spatialdata/sphere.h"
#include "streaming/streamingengine.h"
#include "system/bootmgr.h"
#include "network/NetworkInterface.h"
#include "network/events/NetworkEventTypes.h"
#include "network/live/NetworkTelemetry.h"
#include "system/filemgr.h"
#include "debugaudio.h"
AUDIO_MUSIC_OPTIMISATIONS()
PARAM(disableradiohistory, "[Audio] Disable radio track history");
PARAM(disableuserradio, "[Audio] Disable user radio");
audWaveSlot *g_RadioDjSpeechWaveSlot = NULL;
bool audRadioStation::sm_PlayingEndCredits = false;
f32 g_RadioDjDuckingLevelDb = 8.0f;
f32 g_RadioDjDuckingLevelLin = 0.0f;// computed from duckingleveldb in initclass
u32 g_RadioDjDuckTimeMs = 150;
enum { kMinRadioTracksOutsideHistory = 2};
atRangeArray<u8, kMaxNewsStories> audRadioStation::sm_NewsStoryState;
audRadioStationHistory audRadioStation::sm_NewsHistory;
audRadioStationHistory audRadioStation::sm_WeatherHistory;
audRadioStationHistory audRadioStation::sm_AdvertHistory;
audRadioStationTrackListCollection audRadioStation::sm_NewsTrackLists;
audRadioStationTrackListCollection audRadioStation::sm_WeatherTrackLists;
u32 audRadioStation::sm_PositionedPlayerVehicleRadioLPFCutoff = kVoiceFilterLPFMaxCutoff;
u32 audRadioStation::sm_PositionedPlayerVehicleRadioHPFCutoff = kVoiceFilterHPFMinCutoff;
f32 audRadioStation::sm_PositionedPlayerVehicleRadioEnvironmentalLoudness = 0.f;
f32 audRadioStation::sm_PositionedPlayerVehicleRadioVolume = 0.f;
f32 audRadioStation::sm_PositionedPlayerVehicleRadioRollOff = 1.f;
bool audRadioStation::sm_HasSyncedUnlockableDJStation = false;
float audRadioStation::sm_CountryTalkRadioSignal = 1.f;
float audRadioStation::sm_ScriptGlobalRadioSignalLevel = 1.f;
bool audRadioStation::sm_UpdateLockedStations = false;
#if RSG_PC
bool audRadioStation::sm_IsInRecoveryMode = false;
#endif
bool audRadioStation::sm_HasInitialisedStations = false;
#define AUD_CLEAR_TRISTATE_VALUE(flagvar, flagid) (flagvar &= ~(0 | ((AUD_GET_TRISTATE_VALUE(flagvar, flagid)&0x03) << (flagid<<1))))
#if __BANK && 0
extern bool g_DrawRadioDebug;
extern audRadioStation *g_DebugRadioStation;
#define RADIODEBUG(arg) if(g_DrawRadioDebug && g_DebugRadioStation == this){Printf("[%s] ", GetName());Displayf(arg);}
#define RADIODEBUG2(arg1,arg2) if(g_DrawRadioDebug && g_DebugRadioStation == this){Printf("[%s] ", GetName());Displayf(arg1, arg2);}
#define RADIODEBUG3(arg1, arg2, arg3) if(g_DrawRadioDebug && g_DebugRadioStation == this){Printf("[%s] ", GetName());Displayf(arg1, arg2, arg3);}
#define RADIODEBUG4(arg1, arg2, arg3, arg4) if(g_DrawRadioDebug && g_DebugRadioStation == this){Printf("[%s] ", GetName());Displayf(arg1,arg2,arg3,arg4);}
#else
#define RADIODEBUG(arg)
#define RADIODEBUG2(arg1,arg2)
#define RADIODEBUG3(arg1, arg2, arg3)
#define RADIODEBUG4(arg1, arg2, arg3, arg4)
#endif
BANK_ONLY(bool audRadioStation::sm_DebugHistory = false);
enum audRadioTrackWeatherContexts
{
RADIO_TRACK_WEATHER_CONTEXT_FOG,
RADIO_TRACK_WEATHER_CONTEXT_RAIN,
RADIO_TRACK_WEATHER_CONTEXT_WIND,
RADIO_TRACK_WEATHER_CONTEXT_SUN,
RADIO_TRACK_WEATHER_CONTEXT_CLOUD,
NUM_RADIO_TRACK_WEATHER_CONTEXTS
};
static const u32 g_RadioTrackWeatherContextNames[NUM_RADIO_TRACK_WEATHER_CONTEXTS] =
{
ATSTRINGHASH("FOG", 0xD61BDE01),//RADIO_TRACK_WEATHER_CONTEXT_FOG,
ATSTRINGHASH("RAIN", 0x54A69840),//RADIO_TRACK_WEATHER_CONTEXT_RAIN,
ATSTRINGHASH("WIND", 0x35369828),//RADIO_TRACK_WEATHER_CONTEXT_WIND,
ATSTRINGHASH("SUN", 0x47AE791A),//RADIO_TRACK_WEATHER_CONTEXT_SUN,
ATSTRINGHASH("CLOUD", 0x27F49EC9)//RADIO_TRACK_WEATHER_CONTEXT_CLOUD,
};
bank_s32 g_RadioTrackOverlapPlayTimeMs = 1000;
bank_s32 g_RadioTrackMixtransitionTimeMs = 2000;
bank_s32 g_RadioTrackMixtransitionOffsetTimeMs = 0;
bank_s32 g_RadioTrackOverlapTimeMs = 1000;
bank_s32 g_UserRadioTrackOverlapTimeMs = 500;
bank_s32 g_RadioTrackPrepareTimeMs = 7000;
bank_s32 g_RadioTrackSkipEndTimeMs = 7000;
bool g_UsePreciseTrackCrossfades = true;
bank_u32 g_RadioWheelWindStart = 11000;
bank_u32 g_RadioWheelWindOffset = 50000;
bank_u32 g_RadioDjSpeechMinDelayMsMotomami = 10000;
bank_float g_RadioDjSpeechProbability = 0.8f; //Prob of DJ speech playing within a valid window.
bank_float g_RadioDjSpeechProbabilityKult = 0.7f; //Prob of DJ speech playing within a valid window.
bank_float g_RadioDjOutroSpeechProbabilityMotomami = 0.275f; //Prob of DJ speech playing within a valid window.
bank_float g_RadioDjTimeProbability = 0.5f; //Prob of time banter playing instead of an intro.
const s32 g_RadioDjMinPrepareTimeMs = 3000;
const s32 g_RadioDjPlayTimeTresholdMs = 500;
const u8 g_RadioDjMaxVariations = 100;
const char *g_RadioDjSpeechCategoryNames[NUM_RADIO_DJ_SPEECH_CATS] =
{
"INTRO", //RADIO_DJ_SPEECH_CAT_INTRO
"OUTRO", //RADIO_DJ_SPEECH_CAT_OUTRO
"GENERAL", //RADIO_DJ_SPEECH_CAT_GENERAL
"TIME", //RADIO_DJ_SPEECH_CAT_TIME
"TO" //RADIO_DJ_SPEECH_CAT_TO
};
//The minimum amount of time that must pass (in milliseconds) between repeated playback of a DJ speech category.
const u32 g_RadioDjSpeechMinTimeBetweenRepeatedCategories[NUM_RADIO_DJ_SPEECH_CATS] =
{
0, //RADIO_DJ_SPEECH_CAT_INTRO
0, //RADIO_DJ_SPEECH_CAT_OUTRO
45000, //RADIO_DJ_SPEECH_CAT_GENERAL - 45s, to prevent back to back generals when driven by score
1800000, //RADIO_DJ_SPEECH_CAT_TIME - 30mins
0 //RADIO_DJ_SPEECH_CAT_TO
};
const u32 g_RadioDjSpeechMinTimeBetweenRepeatedCategoriesKult[NUM_RADIO_DJ_SPEECH_CATS] =
{
540000, //RADIO_DJ_SPEECH_CAT_INTRO - 9 mins
0, //RADIO_DJ_SPEECH_CAT_OUTRO
540000, //RADIO_DJ_SPEECH_CAT_GENERAL - 9 mins
1800000, //RADIO_DJ_SPEECH_CAT_TIME - 30mins
0 //RADIO_DJ_SPEECH_CAT_TO
};
const u8 g_RadioDjTimeMorningStart = 5;
const u8 g_RadioDjTimeMorningEnd = 9;
const u8 g_RadioDjTimeAfternoonStart = 12;
const u8 g_RadioDjTimeAfternoonEnd = 16;
const u8 g_RadioDjTimeEveningStart = 17;
const u8 g_RadioDjTimeEveningEnd = 20;
const u8 g_RadioDjTimeNightStart = 21;
const u8 g_RadioDjTimeNightEnd = 3;
bool g_DisablePlayTimeCompensation = false;
const char *g_RadioDjMorningContext = "MORNING";
const char *g_RadioDjAfternoonContext = "AFTERNOON";
const char *g_RadioDjEveningContext = "EVENING";
const char *g_RadioDjNightContext = "NIGHT";
const char *g_RadioDjToAdvertContext = "TO_AD";
const char *g_RadioDjToNewsContext = "TO_NEWS";
const char *g_RadioDjToWeatherContext = "TO_WEATHER";
static char g_RadioDjVoiceName[g_MaxRadioNameLength];
static char g_RadioDjContextName[g_MaxRadioNameLength];
bool audRadioStation::sm_IsNetworkModeHistoryActive = false;
bool audRadioStation::sm_IMDrivenDjSpeech = false;
bool audRadioStation::sm_IMDrivenDjSpeechScheduled = false;
u8 audRadioStation::sm_IMDjSpeechCategory = 0;
audRadioStation *audRadioStation::sm_IMDjSpeechStation = NULL;
const audRadioStation *audRadioStation::sm_DjSpeechStation = NULL;
u32 audRadioStation::sm_DjSpeechNextTrackCategory = 0;
audCurve audRadioStation::sm_VehicleRadioRiseVolumeCurve;
audSimpleSmoother audRadioStation::sm_DjDuckerVolumeSmoother;
audSound *audRadioStation::sm_RetuneMonoSound = NULL;
audSound *audRadioStation::sm_RetunePositionedSound = NULL;
audSound *audRadioStation::sm_DjSpeechMonoSound = NULL;
audSound *audRadioStation::sm_DjSpeechPositionedSound = NULL;
atArray<audRadioStation *> audRadioStation::sm_OrderedRadioStations;
const RadioTrackCategoryData *audRadioStation::sm_MinTimeBetweenRepeatedCategories = NULL;
const RadioTrackCategoryData *audRadioStation::sm_RadioTrackCategoryWeightsFromMusic = NULL;
const RadioTrackCategoryData *audRadioStation::sm_RadioTrackCategoryWeightsFromBackToBackMusic = NULL;
const RadioTrackCategoryData *audRadioStation::sm_RadioTrackCategoryWeightsNoBackToBackMusic = NULL;
const RadioTrackCategoryData *audRadioStation::sm_RadioTrackCategoryWeightsFromBackToBackMusicKult = NULL;
const RadioTrackCategoryData *audRadioStation::sm_RadioTrackCategoryWeightsNoBackToBackMusicKult = NULL;
#if RSG_PC // user music
const RadioTrackCategoryData *audRadioStation::sm_MinTimeBetweenRepeatedCategoriesUser = NULL;
const RadioTrackCategoryData *audRadioStation::sm_UserMusicWeightsFromMusic = NULL;
#endif
#if HEIST3_HIDDEN_RADIO_ENABLED
u32 audRadioStation::sm_Heist3HiddenRadioBankId = ~0u;
#endif
s32 audRadioStation::sm_ActiveDjSpeechStartTimeMs = 0;
u32 audRadioStation::sm_ActiveDjSpeechId = 0;
u8 audRadioStation::sm_ActiveDjSpeechCategory = g_NullDjSpeechCategory;
u32 audRadioStation::sm_NumRadioStations = 0;
u32 audRadioStation::sm_NumUnlockedRadioStations = 0;
u32 audRadioStation::sm_NumUnlockedFavouritedStations = 0;
u8 audRadioStation::sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_IDLE;
bool audRadioStation::sm_DisableOverlap = false;
f32 audRadioStation::sm_DjSpeechFrontendVolume = 0.f;
f32 audRadioStation::sm_DjSpeechFrontendLPF = kVoiceFilterLPFMaxCutoff;
// user music additions to pc version
#if RSG_PC
bool audRadioStation::sm_ForceUserNextTrack = false;
bool audRadioStation::sm_ForceUserPrevTrack = false;
audUserRadioTrackManager audRadioStation::sm_UserRadioTrackManager;
#endif
#if __BANK
bool g_LogRadio = false;
bool g_ForceDjSpeech = false;
bool g_DebugRadioRandom = false;
u32 g_TestSyncStationIndex = 0;
extern f32 g_StationDebugDrawYScroll;
bool g_DebugDrawUSBStation = false;
bool g_HasAddedUSBStationWidgets = false;
u32 g_NumUSBStationTrackLists = 0;
s32 g_RequestedUSBTrackListComboIndex = -1;
u32 g_USBStationTrackListOffsetMs = 0;
atRangeArray<const char*, 20> g_USBTrackListNames;
bkCombo* g_USBStationTrackListCombo = nullptr;
bkGroup* g_USBStationGroup = nullptr;
bkBank* g_USBStationBank = nullptr;
#endif
s32 QSortRadioStationCompareFunc(audRadioStation* const* stationA, audRadioStation* const* stationB)
{
return ((*stationA)->GetOrder() - (*stationB)->GetOrder());
}
void audRadioStation::SortRadioStationList()
{
sm_OrderedRadioStations.QSort(0, sm_NumRadioStations, QSortRadioStationCompareFunc);
}
s32 audRadioStation::GetOrder() const
{
s32 order = 0;
if(m_StationSettings)
{
order = m_StationSettings->Order;
// url:bugstar:7186452 - The ordering of the stations on the radio wheel has changed compared to previous builds
// Slightly hacky fix to restore station ordering now that the off station has changed index. We just take all
// stations past a set order value and bump them to the back of the list, effectively rotating the station wheel
if(order < g_RadioWheelWindStart)
{
order += g_RadioWheelWindOffset;
}
// url:bugstar:7186089 - On the Radio Wheel, is it possible to keep the Media Player next to the Radio Off option,
// even when players have begun to thin the radio wheel down using the favourite station feature?
if(IsUSBMixStation())
{
order = 1;
}
if(!m_IsFavourited)
{
order += 100000;
}
if(AUD_GET_TRISTATE_VALUE(m_StationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_LOCKED) == AUD_TRISTATE_TRUE)
{
order += 2000000;
}
else if(AUD_GET_TRISTATE_VALUE(m_StationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_HIDDEN) == AUD_TRISTATE_TRUE)
{
order += 4000000;
}
}
return order;
}
enum { kMaxRadioStationLists = 16};
void audRadioStation::InitClass(void)
{
//NOTE: This function must be called AFTER all downloadable audio content has been mounted and all game objects have been combined.
// Otherwise downloadable radio content will be ignored.
// HL: This comment was clearly never adhered to! Additional track lists can still be merged via after ::Init has been called. Risky
// to change so have added ::DLCInit fn to do any deferred processing
char radioStationListName[64];
sm_ActiveDjSpeechStartTimeMs = 0;
sm_ActiveDjSpeechId = 0;
sm_ActiveDjSpeechCategory = g_NullDjSpeechCategory;
sm_NumUnlockedRadioStations = 0;
sm_NumUnlockedFavouritedStations = 0;
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_IDLE;
sm_OrderedRadioStations.Resize(g_MaxRadioStations);
sm_MinTimeBetweenRepeatedCategories = audNorthAudioEngine::GetObject<RadioTrackCategoryData>(ATSTRINGHASH("RADIO_TRACK_CATEGORY_MIN_TIME_BETWEEN_REPEATS", 0x4BFB4679));
WIN32PC_ONLY(sm_MinTimeBetweenRepeatedCategoriesUser = audNorthAudioEngine::GetObject<RadioTrackCategoryData>(ATSTRINGHASH("RADIO_TRACK_CATEGORY_TIMES_SELF", 0x26EE6A9A)));
sm_RadioTrackCategoryWeightsFromMusic = audNorthAudioEngine::GetObject<RadioTrackCategoryData>(ATSTRINGHASH("RADIO_TRACK_CATEGORY_WEIGHTS_FROM_MUSIC", 0xA3D15F60));
WIN32PC_ONLY(sm_UserMusicWeightsFromMusic = audNorthAudioEngine::GetObject<RadioTrackCategoryData>(ATSTRINGHASH("RADIO_TRACK_CATEGORY_WEIGHTS_SELF", 0x45F45451)));
sm_RadioTrackCategoryWeightsFromBackToBackMusic = audNorthAudioEngine::GetObject<RadioTrackCategoryData>(ATSTRINGHASH("RADIO_TRACK_CATEGORY_WEIGHTS_FROM_BACK_TO_BACK_MUSIC", 0xDBA8201E));
sm_RadioTrackCategoryWeightsNoBackToBackMusic = audNorthAudioEngine::GetObject<RadioTrackCategoryData>(ATSTRINGHASH("RADIO_TRACK_CATEGORY_WEIGHTS_NO_BACK_TO_BACK_MUSIC", 0xB0EA87F1));
sm_RadioTrackCategoryWeightsNoBackToBackMusic = sm_RadioTrackCategoryWeightsFromBackToBackMusic;
sm_RadioTrackCategoryWeightsFromBackToBackMusicKult = audNorthAudioEngine::GetObject<RadioTrackCategoryData>(ATSTRINGHASH("RADIO_TRACK_CATEGORY_WEIGHTS_FROM_BACK_TO_BACK_MUSIC_KULTFM", 0xAA636584));
sm_RadioTrackCategoryWeightsNoBackToBackMusicKult = audNorthAudioEngine::GetObject<RadioTrackCategoryData>(ATSTRINGHASH("RADIO_TRACK_CATEGORY_WEIGHTS_FROM_MUSIC_KULTFM", 0x8FC9E246));
if (!sm_RadioTrackCategoryWeightsFromBackToBackMusicKult)
{
sm_RadioTrackCategoryWeightsFromBackToBackMusicKult = sm_RadioTrackCategoryWeightsFromBackToBackMusic;
}
if (!sm_RadioTrackCategoryWeightsNoBackToBackMusicKult)
{
sm_RadioTrackCategoryWeightsNoBackToBackMusicKult = sm_RadioTrackCategoryWeightsNoBackToBackMusic;
}
// Radio initialisation overwrites metadata on init, so prevent RAVE edits from screwing with that
BANK_ONLY(const_cast<audMetadataManager&>(audNorthAudioEngine::GetMetadataManager()).DisableEditsForObjectType(RadioStationTrackList::TYPE_ID));
for(u32 listIndex=0; listIndex<kMaxRadioStationLists; listIndex++)
{
strStreamingEngine::GetLoader().CallKeepAliveCallbackIfNecessary();
formatf(radioStationListName, "RADIO_STATIONS_%02u", listIndex + 1);
RadioStationList *radioStationList = audNorthAudioEngine::GetObject<RadioStationList>(radioStationListName);
if(radioStationList == NULL)
{
continue;
}
if(listIndex == 0)
{
//This is the list of shipped radio stations.
InitRadioStationList(radioStationList);
}
else
{
//This a list of downloaded radio stations that needs to be merged into the master list.
MergeRadioStationList(radioStationList);
}
}
sm_VehicleRadioRiseVolumeCurve.Init(ATSTRINGHASH("VEHICLE_RADIO_RISE", 0x5B9052BA));
f32 increaseDecreaseRate = 1.0f / (f32)g_RadioDjDuckTimeMs;
sm_DjDuckerVolumeSmoother.Init(increaseDecreaseRate);
g_RadioDjDuckingLevelLin = audDriverUtil::ComputeLinearVolumeFromDb(g_RadioDjDuckingLevelDb);
g_RadioDjSpeechWaveSlot = audWaveSlot::FindWaveSlot(ATSTRINGHASH("DJ_SPEECH", 0x06b9d8c6a));
const RadioStationTrackList *trackList = NULL;
// Locked news track lists are dealt with differently than others
sm_NewsTrackLists.SetLockingEnabled(false);
for(u32 index = 1; index < 64; index++)
{
char buf[16];
formatf(buf, "RADIO_NEWS_%02d", index);
trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(buf);
if(trackList)
{
Displayf("Adding news track list: %s (%u tracks)", buf, trackList->numTracks);
Assert(trackList->Category == RADIO_TRACK_CAT_NEWS);
sm_NewsTrackLists.AddTrackList(trackList);
}
}
trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(ATSTRINGHASH("RADIO_WEATHER_01", 0x09a5d886f));
Assert(trackList && trackList->Category == RADIO_TRACK_CAT_WEATHER);
sm_WeatherTrackLists.AddTrackList(trackList);
for(u32 index = 2; index < 64; index++)
{
char buf[32];
formatf(buf, "RADIO_WEATHER_%02d", index);
trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(buf);
if(trackList)
{
Displayf("Adding weather track list: %s (%u tracks)", buf, trackList->numTracks);
Assert(trackList->Category == RADIO_TRACK_CAT_WEATHER);
sm_WeatherTrackLists.AddTrackList(trackList);
}
}
Assert(sm_NewsTrackLists.ComputeNumTracks() < kMaxNewsStories);
#if RSG_PC
sm_ForceUserNextTrack = false;
sm_ForceUserPrevTrack = false;
sm_UserRadioTrackManager.Initialise();
if( CPauseMenu::GetMenuPreference(PREF_UR_AUTOSCAN) )
{
// Quick scan only on boot
sm_UserRadioTrackManager.StartMediaScan(false);
while(sm_UserRadioTrackManager.IsScanning())
{
sysIpcSleep(10);
}
sm_UserRadioTrackManager.UpdateScanningUI(false);
}
sm_UserRadioTrackManager.LoadFileLookup();
// Create a user music station
RadioStationSettings *stationSettings = audNorthAudioEngine::GetObject<RadioStationSettings>("RADIO_19_USER");
if(stationSettings)
{
sm_OrderedRadioStations[sm_NumRadioStations] = rage_new audRadioStation(stationSettings);
sm_OrderedRadioStations[sm_NumRadioStations]->SetLocked(!HasUserTracks() || PARAM_disableuserradio.Get());
sm_NumRadioStations++;
}
#endif // RSG_PC
//Order radio stations.
sm_OrderedRadioStations.QSort(0, sm_NumRadioStations, QSortRadioStationCompareFunc);
for(u32 stationIndex=0; stationIndex < sm_NumRadioStations; stationIndex++)
{
if(sm_OrderedRadioStations[stationIndex])
{
sm_OrderedRadioStations[stationIndex]->Init();
if(!sm_OrderedRadioStations[stationIndex]->IsLocked() && !sm_OrderedRadioStations[stationIndex]->IsHidden())
{
sm_NumUnlockedRadioStations++;
if(sm_OrderedRadioStations[stationIndex]->IsFavourited())
{
sm_NumUnlockedFavouritedStations++;
}
}
}
}
// Flag that any stations added after this point (ie via DLC rather than patch packs/core) require explicit init
sm_HasInitialisedStations = true;
#if __BANK
if (!sysBootManager::IsPackagedBuild())
{
FileHandle fi = CFileMgr::OpenFileForWriting("common:/RadioLog.csv");
if(fi != INVALID_FILE_HANDLE)
{
const char *header = "Station,TimeInMs,Category,Index,Context,Sound\r\n";
CFileMgr::Write(fi, header, istrlen(header));
CFileMgr::CloseFile(fi);
}
}
#endif
}
void audRadioStation::PostLoad()
{
#if RSG_PC
if(m_IsUserStation)
{
sm_UserRadioTrackManager.PostLoad();
}
#endif
}
#if RSG_PC
bool audRadioStation::HasUserTracks()
{
return sm_UserRadioTrackManager.HasTracks();
}
#endif
s32 QSortBeatMarkerCompareFunc(audRadioStation::MixBeatMarker const* beatMarkerA, audRadioStation::MixBeatMarker const* beatMarkerB)
{
s32 playTimeA = (*beatMarkerA).playtimeMs;
s32 playTimeB = (*beatMarkerB).playtimeMs;
s32 trackA = (s32)((*beatMarkerA).trackIndex);
s32 trackB = (s32)((*beatMarkerB).trackIndex);
if (playTimeA != playTimeB)
{
return playTimeA - playTimeB;
}
else
{
return trackA - trackB;
}
}
void audRadioStation::ConstructMixStationBeatMarkerList()
{
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC);
if (trackLists)
{
const u32 numTracks = trackLists->ComputeNumTracks(0u, true);
for (u32 trackIndex = 0; trackIndex < numTracks; trackIndex++)
{
const u32 soundRef = trackLists->GetTrack(trackIndex, 0u, true)->SoundRef;
u32 totalStationDuration = 0;
u32 trackStartOffsetMs = 0;
u32 thisTrackDuration = 0;
// Iterate over all the tracks and grab the total station duration, duration of the given track, and offset of the given track
for (u32 i = 0; i < numTracks; i++)
{
const StreamingSound *streamingSound = SOUNDFACTORY.DecompressMetadata<StreamingSound>(trackLists->GetTrack(i, 0u, true)->SoundRef);
if (streamingSound)
{
if (i < trackIndex)
{
trackStartOffsetMs += streamingSound->Duration;
}
if (i == trackIndex)
{
thisTrackDuration = streamingSound->Duration;
}
totalStationDuration += streamingSound->Duration;
m_MixStationBeatMarkerTrackDurations.PushAndGrow(streamingSound->Duration);
}
}
char textIdObjName[16];
formatf(textIdObjName, "RTB_%08X", audRadioTrack::GetBeatSoundName(soundRef));
const RadioTrackTextIds *beatMarkers = audNorthAudioEngine::GetObject<RadioTrackTextIds>(textIdObjName);
if (beatMarkers)
{
u32 numBeatMarkers = beatMarkers->numTextIds;
// Now add all the beat markers for the current track, offset by the tracks position into the mix
for (u32 i = 0; i < numBeatMarkers; i++)
{
MixBeatMarker mixBeatMarker;
mixBeatMarker.numBeats = beatMarkers->TextId[i].TextId;
mixBeatMarker.playtimeMs = beatMarkers->TextId[i].OffsetMs + trackStartOffsetMs;
mixBeatMarker.trackIndex = trackIndex;
m_MixStationBeatMarkers.PushAndGrow(mixBeatMarker);
}
// Duplicate all the first track's beat markers at the end of the beat marker list
if (trackIndex == 0)
{
for (u32 i = 0; i < numBeatMarkers; i++)
{
MixBeatMarker mixBeatMarker;
mixBeatMarker.numBeats = beatMarkers->TextId[i].TextId;
mixBeatMarker.playtimeMs = beatMarkers->TextId[i].OffsetMs + totalStationDuration;
mixBeatMarker.trackIndex = trackIndex;
m_MixStationBeatMarkers.PushAndGrow(mixBeatMarker);
}
}
// Duplicate all the last track's beat markers at the beginning of the beat marker list
if (trackIndex == numTracks - 1)
{
for (u32 i = 0; i < numBeatMarkers; i++)
{
MixBeatMarker mixBeatMarker;
mixBeatMarker.numBeats = beatMarkers->TextId[i].TextId;
mixBeatMarker.playtimeMs = beatMarkers->TextId[i].OffsetMs - thisTrackDuration;
mixBeatMarker.trackIndex = trackIndex;
m_MixStationBeatMarkers.PushAndGrow(mixBeatMarker);
}
}
}
}
}
m_MixStationBeatMarkers.QSort(0, m_MixStationBeatMarkers.GetCount(), QSortBeatMarkerCompareFunc);
}
#if __BANK
void audRadioStation::DebugDrawMixBeatMarkers() const
{
char tempString[256];
f32 yStepRate = 0.015f;
f32 xCoord = 0.05f;
f32 yCoord = 0.05f - g_StationDebugDrawYScroll;
f32 yCoordUnscrolled = 0.05f;
const audRadioTrack& beatTrack = GetMixStationBeatTrack();
formatf(tempString, "Currently audible track: %s (ZiT: %u)", beatTrack.GetBankName(), beatTrack.GetTextId());
grcDebugDraw::Text(Vector2(0.2f, yCoordUnscrolled), Color32(255, 255, 255), tempString);
yCoordUnscrolled += yStepRate;
formatf(tempString, "(Play time: %d/%u, %.02f%%, %d remaining)", beatTrack.GetPlayTime(), beatTrack.GetDuration(), (beatTrack.GetPlayTime() / (f32)beatTrack.GetDuration()) * 100.f, beatTrack.GetDuration() - beatTrack.GetPlayTime());
grcDebugDraw::Text(Vector2(0.2f, yCoordUnscrolled), Color32(255, 255, 255), tempString);
yCoordUnscrolled += yStepRate;
float bpm;
s32 beatNumber;
float beatTimeS;
if (beatTrack.GetNextBeat(beatTimeS, bpm, beatNumber))
{
formatf(tempString, "Audible beat: %.2f (%u/4 %.1f BPM)", beatTimeS, beatNumber, bpm);
grcDebugDraw::Text(Vector2(0.2f, yCoordUnscrolled), Color32(255, 255, 255), tempString);
}
else
{
formatf(tempString, "No audible beat found!");
grcDebugDraw::Text(Vector2(0.2f, yCoordUnscrolled), Color32(255, 255, 255), tempString);
}
yCoordUnscrolled += yStepRate;
s32 currentPlaytimeInMs = beatTrack.GetPlayTime();
for (u32 i = 0; i < beatTrack.GetTrackIndex(); i++)
{
currentPlaytimeInMs += m_MixStationBeatMarkerTrackDurations[i];
}
s32 nearestEarlierMarkerTimeMs = -INT_MAX;
s32 nearestLaterMarkerTimeMs = INT_MAX;
s32 nearestEarlierMarkerIndex = -1;
s32 nearestLaterMarkerIndex = -1;
for (u32 i = 0; i < m_MixStationBeatMarkers.GetCount(); i++)
{
if (m_MixStationBeatMarkers[i].playtimeMs >= nearestEarlierMarkerTimeMs && m_MixStationBeatMarkers[i].playtimeMs <= currentPlaytimeInMs)
{
nearestEarlierMarkerTimeMs = m_MixStationBeatMarkers[i].playtimeMs;
nearestEarlierMarkerIndex = i;
}
else if (m_MixStationBeatMarkers[i].playtimeMs > currentPlaytimeInMs && m_MixStationBeatMarkers[i].playtimeMs < nearestLaterMarkerTimeMs)
{
nearestLaterMarkerTimeMs = m_MixStationBeatMarkers[i].playtimeMs;
nearestLaterMarkerIndex = i;
}
}
formatf(tempString, "Track Relative Play Time: %d", currentPlaytimeInMs);
grcDebugDraw::Text(Vector2(0.2f, yCoordUnscrolled), Color32(255, 255, 255), tempString);
yCoordUnscrolled += yStepRate;
for (s32 i = 0; i < m_MixStationBeatMarkers.GetCount(); i++)
{
Color32 color = Color32(255, 255, 255);
if (i == nearestEarlierMarkerIndex) { color = Color32(255, 128, 255); }
else if (i == nearestLaterMarkerIndex) { color = Color32(128, 255, 255); }
formatf(tempString, "%d : %d (Track %d)", m_MixStationBeatMarkers[i].playtimeMs, m_MixStationBeatMarkers[i].numBeats, m_MixStationBeatMarkers[i].trackIndex);
grcDebugDraw::Text(Vector2(xCoord, yCoord), color, tempString);
yCoord += yStepRate;
}
}
#endif
const audRadioTrack& audRadioStation::GetMixStationBeatTrack() const
{
// Detect when we've transitioned to the next track already
if (!GetCurrentTrack().IsStreamingSoundValid() && GetNextTrack().IsStreamingSoundValid())
{
return GetNextTrack();
}
return GetCurrentTrack();
}
bool audRadioStation::GetNextMixStationBeat(f32& beatTimeS, f32& bpm, s32& beatNumber)
{
const audRadioTrack& beatTrack = GetMixStationBeatTrack();
u32 trackIndex = beatTrack.GetTrackIndex();
s32 currentPlaytimeInMs = Max(0, beatTrack.GetPlayTime());
// Convert our local play time into mix play time
if (trackIndex < m_MixStationBeatMarkerTrackDurations.GetCount())
{
for (u32 i = 0; i < trackIndex; i++)
{
currentPlaytimeInMs += m_MixStationBeatMarkerTrackDurations[i];
}
const f32 currentPlaytimeInSeconds = currentPlaytimeInMs*0.001f;
s32 nearestEarlierMarkerTimeMs = -INT_MAX;
s32 nearestLaterMarkerTimeMs = INT_MAX;
u32 numBeats = 0;
for (u32 i = 0; i < m_MixStationBeatMarkers.GetCount(); i++)
{
s32 markerPlayTimeMs = m_MixStationBeatMarkers[i].playtimeMs;
// If this is the next track in the sequence, treat it as though it started 1000ms earlier to cope with the overlap period
if (m_MixStationBeatMarkers[i].trackIndex == trackIndex + 1 % m_MixStationBeatMarkerTrackDurations.GetCount())
{
markerPlayTimeMs -= g_RadioTrackOverlapTimeMs;
}
// If this is the previous track in the sequence, treat it as though it started 1000ms later to cope with the overlap period
else if(m_MixStationBeatMarkers[i].trackIndex == (trackIndex + m_MixStationBeatMarkerTrackDurations.GetCount() - 1) % m_MixStationBeatMarkerTrackDurations.GetCount())
{
markerPlayTimeMs += g_RadioTrackOverlapTimeMs;
}
if (markerPlayTimeMs >= nearestEarlierMarkerTimeMs && markerPlayTimeMs <= currentPlaytimeInMs)
{
nearestEarlierMarkerTimeMs = markerPlayTimeMs;
numBeats = m_MixStationBeatMarkers[i].numBeats;
}
else if (markerPlayTimeMs > currentPlaytimeInMs && markerPlayTimeMs < nearestLaterMarkerTimeMs)
{
nearestLaterMarkerTimeMs = markerPlayTimeMs;
}
}
const f32 nearestBarTimeInSeconds = nearestEarlierMarkerTimeMs*0.001f;
const f32 timeSinceBar = currentPlaytimeInSeconds - nearestBarTimeInSeconds;
if (numBeats == 0)
{
bpm = 0.f;
beatNumber = 0;
beatTimeS = 0.f;
return false;
}
// markers occur n beats
const f32 beatDuration = (nearestLaterMarkerTimeMs - nearestEarlierMarkerTimeMs) / (numBeats * 1000.f);
bpm = 60.f / beatDuration;
const f32 beatsPastLastBar = timeSinceBar / beatDuration;
const f32 currentBeat = Floorf(beatsPastLastBar);
beatNumber = 1 + ((u32)currentBeat) % 4;
const f32 timeSinceLastBeat = (beatsPastLastBar - currentBeat) * beatDuration;
beatTimeS = beatDuration - timeSinceLastBeat;
}
return true;
}
void audRadioStation::InitRadioStationList(RadioStationList *radioStationList)
{
for(u32 stationIndex=0; stationIndex < radioStationList->numStations; stationIndex++)
{
RadioStationSettings *stationSettings =
audNorthAudioEngine::GetObject<RadioStationSettings>(radioStationList->Station[stationIndex].StationSettings);
if(stationSettings)
{
//Create a new radio station using these settings.
sm_OrderedRadioStations[sm_NumRadioStations] = rage_new audRadioStation(stationSettings);
sm_NumRadioStations++;
}
}
}
void audRadioStation::MergeRadioStationList(const RadioStationList *radioStationList)
{
bool needSort = false;
for(u32 stationIndex = 0; stationIndex < radioStationList->numStations; stationIndex++)
{
RadioStationSettings *stationSettings =
audNorthAudioEngine::GetObject<RadioStationSettings>(radioStationList->Station[stationIndex].StationSettings);
audRadioStation *station = FindStation(stationSettings->Name);
if(station)
{
//These are additional tracks for an existing station.
//Append the tracklists.
for(u32 trackListIndex = 0; trackListIndex < stationSettings->numTrackLists; trackListIndex++)
{
const RadioStationTrackList *trackList =
audNorthAudioEngine::GetObject<RadioStationTrackList>(stationSettings->TrackList[trackListIndex].TrackListRef);
if (trackList && trackList->Category == RADIO_TRACK_CAT_MUSIC)
{
station->AddMusicTrackList(trackList, stationSettings->TrackList[trackListIndex].TrackListRef);
}
else
{
station->AddTrackList(trackList);
}
if (trackList->Category != RADIO_TRACK_CAT_MUSIC)
{
if(sm_IsNetworkModeHistoryActive)
{
station->m_HasTakeoverContent |= (trackList->Category == RADIO_TRACK_CAT_TAKEOVER_MUSIC);
}
}
}
if(station->m_HasTakeoverContent)
{
station->m_UseRandomizedStrideSelection = true;
}
if(station->m_UseRandomizedStrideSelection)
{
station->RandomizeCategoryStrides(true, false);
}
}
else
{
//This is a new station.
//Create a new radio station using these settings.
audRadioStation *station = rage_new audRadioStation(stationSettings);
if(sm_OrderedRadioStations.GetCount() == 0)
{
sm_OrderedRadioStations.Resize(g_MaxRadioStations);
}
sm_OrderedRadioStations[sm_NumRadioStations] = station;
sm_NumRadioStations++;
if(sm_HasInitialisedStations)
{
station->Init();
needSort = true;
}
}
}
if(needSort)
{
sm_OrderedRadioStations.QSort(0, sm_NumRadioStations, QSortRadioStationCompareFunc);
}
#if HEIST3_HIDDEN_RADIO_ENABLED
sm_Heist3HiddenRadioBankId = audWaveSlot::FindBankId(HEIST3_HIDDEN_RADIO_BANK_HASH);
#endif
BANK_ONLY(audRadioAudioEntity::PopulateRetuneWidgetStationNames();)
}
void audRadioStation::RemoveRadioStationList(const RadioStationList *radioStationList)
{
for(u32 stationIndex = 0; stationIndex < radioStationList->numStations; stationIndex++)
{
const RadioStationSettings *stationSettings =
audNorthAudioEngine::GetObject<RadioStationSettings>(radioStationList->Station[stationIndex].StationSettings);
audRadioStation *station = FindStation(stationSettings->Name);
if(station)
{
audMetadataObjectInfo objectInfo;
bool isCoreStation = audNorthAudioEngine::GetMetadataManager().GetObjectInfoFromSpecificChunk(station->GetNameHash(), 0, objectInfo);
audRadioSlot *slot = audRadioSlot::FindSlotByStation(station);
if(slot)
{
audWarningf("Stopping associated slot");
slot->InvalidateStation();
}
if (isCoreStation)
{
// url:bugstar:6846455
// If the station existed in core and DLC data, we want to fall back to the core version
// of the station rather than deleting it completely (as the core data will never be re-mounted)
audWarningf("Reverting DLC station %s to core version", stationSettings->Name);
const s32 index = sm_OrderedRadioStations.Find(station);
station->Shutdown();
delete station;
if (audVerifyf(index >= 0, "Failed to find deleted station in station settings list!"))
{
RadioStationSettings* coreStationSettings = const_cast<RadioStationSettings*>(objectInfo.GetObject<RadioStationSettings>());
sm_OrderedRadioStations[index] = rage_new audRadioStation(coreStationSettings);
sm_OrderedRadioStations[index]->Init();
}
}
else
{
audWarningf("Removing DLC station %s", stationSettings->Name);
// This was a new station, so just remove it
sm_OrderedRadioStations.DeleteMatches(station);
sm_OrderedRadioStations.Resize(g_MaxRadioStations);
station->Shutdown();
delete station;
sm_NumRadioStations--;
}
}
}
}
void audRadioStation::AddMusicTrackList(const RadioStationTrackList *trackList, audMetadataRef metadataRef)
{
audDisplayf("Adding music track list %s on station %s", audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackList->NameTableOffset), GetName());
u32 trackListHash = audNorthAudioEngine::GetMetadataManager().GetObjectHashFromMetadataRef(metadataRef);
audAssert(trackListHash == atStringHash(audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackList->NameTableOffset)));
f32 trackListProbability = 1.f;
if (Tunables::GetInstance().Access(BASE_GLOBALS_HASH, trackListHash, trackListProbability))
{
audDisplayf("Custom first track probability tuneable (%.02f) found for track list %s on station %s",
trackListProbability,
audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackList->NameTableOffset),
GetName());
}
AddTrackList(trackList, trackListProbability);
}
void audRadioStation::AddTrackList(const RadioStationTrackList *trackList, f32 firstTrackProbability)
{
if(!audVerifyf(trackList, "NULL tracklist passed to %s", GetName()))
{
return;
}
audDisplayf("Adding %s track list %s on station %s", TrackCats_ToString((TrackCats)trackList->Category), audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackList->NameTableOffset), GetName());
switch(trackList->Category)
{
case RADIO_TRACK_CAT_DJSOLO:
case RADIO_TRACK_CAT_IDENTS:
case RADIO_TRACK_CAT_MUSIC:
case RADIO_TRACK_CAT_ADVERTS:
case RADIO_TRACK_CAT_TO_AD:
case RADIO_TRACK_CAT_TO_NEWS:
case RADIO_TRACK_CAT_USER_INTRO:
case RADIO_TRACK_CAT_INTRO_MORNING:
case RADIO_TRACK_CAT_INTRO_EVENING:
case RADIO_TRACK_CAT_USER_OUTRO:
case RADIO_TRACK_CAT_TAKEOVER_MUSIC:
case RADIO_TRACK_CAT_TAKEOVER_DJSOLO:
case RADIO_TRACK_CAT_TAKEOVER_IDENTS:
{
audRadioStationTrackListCollection *trackLists = const_cast<audRadioStationTrackListCollection*>(FindTrackListsForCategory(static_cast<s32>(trackList->Category)));
if(trackLists)
{
audVerifyf(trackLists->AddTrackList(trackList, firstTrackProbability), "Failed to add tracklist %s to station %s (duplicate?)",
audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackList->NameTableOffset),
GetName());
}
}
if(trackList->Category != RADIO_TRACK_CAT_ADVERTS)
{
m_OnlyPlayAds = false;
}
break;
// Shared track lists are handled separately
case RADIO_TRACK_CAT_WEATHER:
case RADIO_TRACK_CAT_NEWS:
break;
}
}
void audRadioStation::ResetHistoryForNewGame()
{
audDebugf1("Radio: Resetting history for new game");
// Reset news state based on track lists
u32 curIndex = 0;
for(s32 listIndex = 0; listIndex < sm_NewsTrackLists.GetListCount(); listIndex++)
{
const RadioStationTrackList *trackList = sm_NewsTrackLists.GetList(listIndex);
for(u32 i = 0; i < trackList->numTracks; i++)
{
Assign(sm_NewsStoryState[i + curIndex], (AUD_GET_TRISTATE_VALUE(trackList->Flags, FLAG_ID_RADIOSTATIONTRACKLIST_LOCKED) == AUD_TRISTATE_TRUE ? RADIO_NEWS_ONCE_ONLY : RADIO_NEWS_UNLOCKED));
}
curIndex += trackList->numTracks;
}
for(s32 stationIndex = 0; stationIndex < sm_OrderedRadioStations.GetCount(); stationIndex++)
{
audRadioStation *station = GetStation(stationIndex);
if(station)
{
for(s32 category = RADIO_TRACK_CAT_ADVERTS; category < NUM_RADIO_TRACK_CATS; category++)
{
audRadioStationTrackListCollection *trackLists = const_cast<audRadioStationTrackListCollection*>(station->FindTrackListsForCategory(category));
if(trackLists)
{
trackLists->ResetLockState();
}
}
}
}
sm_PlayingEndCredits = false;
Reset();
}
void audRadioStation::ResetHistoryForNetworkGame()
{
// Only reset when transitioning into MP from SP
if(!sm_IsNetworkModeHistoryActive)
{
audDebugf1("Radio: Resetting history for network game");
sm_IsNetworkModeHistoryActive = true;
for(u32 index=0; index<sm_NumRadioStations; index++)
{
audRadioStation *station = GetStation(index);
if(station WIN32PC_ONLY(&& !station->IsUserStation()) && station->GetNameHash() != ATSTRINGHASH("RADIO_22_DLC_BATTLE_MIX1_RADIO", 0xF8BEAA16))
{
if(!station->m_PlaySequentialMusicTracks && !station->m_UseRandomizedStrideSelection)
{
station->m_MusicTrackHistory.Reset();
}
s32 activeTrackIndex = station->GetMusicTrackListActiveTrackIndex();
f32 trackOffset = station->GetCurrentPlayFractionOfActiveTrack();
// Need to shutdown active tracks to ensure we generate network compatible track IDs for history
// This also resets the FirstTrack flag ensuring we randomise the start position on the first track (important for mix stations)
station->Shutdown();
if (station->m_PlaySequentialMusicTracks)
{
bool trackListStateChanged = false;
// Probably safe to do this for all tracks, but I'm paranoid about accidentally unlocking something that was intentionally locked!
if (station->GetNameHash() == ATSTRINGHASH("RADIO_14_DANCE_02", 0xC7A2843A) || station->GetNameHash() == ATSTRINGHASH("RADIO_13_JAZZ", 0x5A254955))
{
const audRadioStationTrackListCollection* trackLists = station->FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC);
for (s32 trackListIndex = 0; trackListIndex < trackLists->GetListCount(); trackListIndex++)
{
if (trackLists->GetListFirstTrackProbability(trackListIndex) == -1)
{
if (!trackLists->IsListLocked(trackListIndex))
{
audDisplayf("%s is locking unlocked track list %d due to tuneable", station->GetName(), trackListIndex);
station->LockTrackList(trackLists->GetList(trackListIndex));
trackListStateChanged = true;
}
}
else
{
if (trackLists->IsListLocked(trackListIndex))
{
audDisplayf("%s is unlocking locked track list %d due to tuneable", station->GetName(), trackListIndex);
station->UnlockTrackList(trackLists->GetList(trackListIndex));
trackListStateChanged = true;
}
}
}
}
if (trackListStateChanged)
{
audDisplayf("Track list state changed on %s, re-randomizing", station->GetName());
station->RandomizeSequentialStationPlayPosition();
}
else
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(station->FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC));
(*history)[0] = activeTrackIndex;
station->m_FirstTrackStartOffset = trackOffset;
}
}
}
}
// GTA V nextgen B*2118827 fix
audRadioStation *radioStation = audRadioStation::FindStation(ATSTRINGHASH("RADIO_16_SILVERLAKE", 0x5E61F5CF));
if(audVerifyf(radioStation, "Invalid radio station name - RADIO_16_SILVERLAKE"))
{
if(!radioStation->UnlockTrackList("MIRRORPARK_LOCKED"))
{
audWarningf("Failed to unlock tracklist MIRRORPARK_LOCKED on RADIO_16_SILVERLAKE");
}
}
}
}
void audRadioStation::ResetHistoryForSPGame()
{
audDebugf1("Radio: Resetting history for SP game");
if(sm_IsNetworkModeHistoryActive)
{
sm_IsNetworkModeHistoryActive = false;
Reset();
}
// GTA V nextgen B*2118827 fix
audRadioStation *radioStation = audRadioStation::FindStation(ATSTRINGHASH("RADIO_16_SILVERLAKE", 0x5E61F5CF));
if(audVerifyf(radioStation, "Invalid radio station name - RADIO_16_SILVERLAKE"))
{
if(!radioStation->LockTrackList("MIRRORPARK_LOCKED"))
{
audWarningf("Failed to lock tracklist MIRRORPARK_LOCKED on RADIO_16_SILVERLAKE");
}
}
MaintainHeist4SPStationState();
for (u32 i = 0; i < sm_NumRadioStations; i++)
{
if (GetStation(i)->m_PlaySequentialMusicTracks)
{
GetStation(i)->RandomizeSequentialStationPlayPosition();
}
}
}
void audRadioStation::MaintainHeist4SPStationState()
{
audDisplayf("Maintaining WWFM/Flylo Radio state for SP");
// TEMP - Force SP stations to remain in their original configuration regardless of what the tunables say
// url:bugstar:6819975 - Block FlyLo and WWFM additional content in SP
audRadioStation* radioStation = audRadioStation::FindStation(ATSTRINGHASH("RADIO_14_DANCE_02", 0xC7A2843A));
if (radioStation)
{
radioStation->UnlockTrackList(ATSTRINGHASH("RADIO_14_DANCE_02_MUSIC", 0xA53FB7D7));
radioStation->LockTrackList(ATSTRINGHASH("FLYLO_ISLAND_UPDATE_MUSIC_MUSIC", 0x5DD0DB99));
}
radioStation = audRadioStation::FindStation(ATSTRINGHASH("RADIO_13_JAZZ", 0x5A254955));
if (radioStation)
{
radioStation->UnlockTrackList(ATSTRINGHASH("RADIO_13_JAZZ_MUSIC_EDITED", 0x3E56DC28));
radioStation->LockTrackList(ATSTRINGHASH("WWFM_ISLAND_UPDATE_MUSIC_MUSIC", 0x81757B19));
}
}
void audRadioStation::ShutdownClass(void)
{
Reset();
sm_OrderedRadioStations.Reset();
sm_NumRadioStations = 0;
sm_NumUnlockedRadioStations = 0;
sm_NumUnlockedFavouritedStations = 0;
}
bool audRadioStation::IsRandomizedStrideCategory(const u32 category)
{
// Takeover station stores the last chosen track for each valid category into the sync packet history slot array. Because
// we have more categories than we have history slots (we also need to steal some slots for storing next takeover time, .etc),
// we need to selectively sync only those categories that we actually need, otherwise we'll run out of space
if (category == RADIO_TRACK_CAT_MUSIC ||
category == RADIO_TRACK_CAT_IDENTS ||
category == RADIO_TRACK_CAT_TAKEOVER_DJSOLO ||
category == RADIO_TRACK_CAT_TAKEOVER_IDENTS ||
category == RADIO_TRACK_CAT_TAKEOVER_MUSIC ||
category == RADIO_TRACK_CAT_DJSOLO)
{
return true;
}
return false;
}
audRadioStation::audRadioStationSyncData g_TestSyncData;
void audRadioStation::SyncStation(const audRadioStationSyncData &data)
{
if(!sm_IsNetworkModeHistoryActive)
{
audDisplayf("Radio station attempting to sync, but not in network history mode. Resetting history for network game");
ResetHistoryForNetworkGame();
}
if(m_OnlyPlayAds || IsUSBMixStation() || IsLocalPlayerNewsStation() WIN32PC_ONLY(|| m_IsUserStation))
return;
if(m_IsFrozen)
{
audDisplayf("%s - skipping sync as station is currently frozen", GetName());
return;
}
if(!audVerifyf(sm_IsNetworkModeHistoryActive, "Attempting to sync radio station %s, but we're not in network radio mode! Aborting.", GetName()))
{
return;
}
audDisplayf("Received radio station sync data for station %s (Local Player: %s, Host: %s, Synced Time: %u):", GetName(), NetworkInterface::GetLocalGamerName(), NetworkInterface::GetHostName(), NetworkInterface::GetSyncedTimeInMilliseconds());
// Play a burst of retune noise to cover the transition
if(g_RadioAudioEntity.IsPlayerRadioActive() && g_RadioAudioEntity.GetPlayerRadioStation() == this)
{
UpdateRetuneSounds();
}
m_Random.Reset(data.randomSeed);
m_IsFirstTrack = false;
s32 category;
u32 index;
if (m_UseRandomizedStrideSelection)
{
u32 historyIndex = 0;
// Current track is stored in history index 0
category = 0xFFFF & UnpackCategoryFromNetwork(data.trackHistory[historyIndex].category);
index = data.trackHistory[historyIndex++].index;
audRadioStationPackedInt packedAccumulatedPlayTime;
audRadioStationPackedInt packedLastTakeoverTime;
// History index 1-8 store some additional packed data required for takeover stations
packedAccumulatedPlayTime.PackedData.m_Byte0 = data.trackHistory[historyIndex].index;
m_MusicRunningCount = data.trackHistory[historyIndex++].category;
packedAccumulatedPlayTime.PackedData.m_Byte1 = data.trackHistory[historyIndex].index;
m_TakeoverMusicTrackCounter = data.trackHistory[historyIndex++].category;
packedAccumulatedPlayTime.PackedData.m_Byte2 = data.trackHistory[historyIndex].index;
m_CategoryStride[RADIO_TRACK_CAT_MUSIC] = ((s8)data.trackHistory[historyIndex++].category - 8);
packedAccumulatedPlayTime.PackedData.m_Byte3 = data.trackHistory[historyIndex].index;
m_CategoryStride[RADIO_TRACK_CAT_IDENTS] = ((s8)data.trackHistory[historyIndex++].category - 8);
packedLastTakeoverTime.PackedData.m_Byte0 = data.trackHistory[historyIndex].index;
m_CategoryStride[RADIO_TRACK_CAT_TAKEOVER_DJSOLO] = ((s8)data.trackHistory[historyIndex++].category - 8);
packedLastTakeoverTime.PackedData.m_Byte1 = data.trackHistory[historyIndex].index;
m_CategoryStride[RADIO_TRACK_CAT_TAKEOVER_IDENTS] = ((s8)data.trackHistory[historyIndex++].category - 8);
packedLastTakeoverTime.PackedData.m_Byte2 = data.trackHistory[historyIndex].index;
m_CategoryStride[RADIO_TRACK_CAT_TAKEOVER_MUSIC] = ((s8)data.trackHistory[historyIndex++].category - 8);
packedLastTakeoverTime.PackedData.m_Byte3 = data.trackHistory[historyIndex].index;
m_CategoryStride[RADIO_TRACK_CAT_DJSOLO] = ((s8)data.trackHistory[historyIndex++].category - 8);
m_StationAccumulatedPlayTimeMs = packedAccumulatedPlayTime.m_Value;
m_TakeoverLastTimeMs = packedLastTakeoverTime.m_Value;
audDisplayf("Accumulated Play Time: %u", m_StationAccumulatedPlayTimeMs);
audDisplayf("Last Takeover Time: %u", m_TakeoverLastTimeMs);
audDisplayf("Takeover Music Track Count: %u", m_TakeoverMusicTrackCounter);
audDisplayf("Music Run Count: %u", m_MusicRunningCount);
// The rest of the history stores our actual per-category data
for (; historyIndex < data.trackHistory.GetMaxCount(); historyIndex++)
{
const u32 unpackedCategory = 0xFFFF & UnpackCategoryFromNetwork(data.trackHistory[historyIndex].category);
if(unpackedCategory == 0xffff)
{
continue;
}
audRadioStationHistory* history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(unpackedCategory));
if (history)
{
history->Reset();
(*history)[0] = data.trackHistory[historyIndex].index != 0xff ? data.trackHistory[historyIndex].index : ~0u;
history->SetWriteIndex(1);
#if __BANK
u32 soundHash = g_NullSoundHash;
const audRadioStationTrackListCollection* trackList = unpackedCategory < NUM_RADIO_TRACK_CATS ? FindTrackListsForCategory(unpackedCategory) : NULL;
if (trackList && data.trackHistory[historyIndex].index < trackList->ComputeNumTracks())
{
soundHash = trackList->GetTrack(data.trackHistory[historyIndex].index)->SoundRef;
}
audDisplayf("History %d: %s (%s track %u)", historyIndex, SOUNDFACTORY.GetMetadataManager().GetObjectName(soundHash), unpackedCategory >= NUM_RADIO_TRACK_CATS ? "Invalid" : TrackCats_ToString((TrackCats)unpackedCategory), data.trackHistory[historyIndex].index);
#endif
}
}
}
else
{
// Also reset history
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC));
if (history)
{
history->Reset();
// We only receive one history record (representing the currently playing track) for sequential music stations
for (s32 i = 0; i < (m_PlaySequentialMusicTracks ? 1 : data.trackHistory.GetMaxCount()); i++)
{
// category in low 16bits, index in high 16 bits
const u32 category = 0xFFFF & UnpackCategoryFromNetwork(data.trackHistory[i].category);
const u32 trackIndex = (data.trackHistory[i].index << 16);
(*history)[i] = (category) | trackIndex;
#if __BANK
u32 soundHash = g_NullSoundHash;
const u32 unpackedCategory = UnpackCategoryFromNetwork(data.trackHistory[i].category);
const audRadioStationTrackListCollection* trackList = unpackedCategory < NUM_RADIO_TRACK_CATS ? FindTrackListsForCategory(unpackedCategory) : NULL;
if (trackList && data.trackHistory[i].index < trackList->ComputeNumTracks())
{
soundHash = trackList->GetTrack(data.trackHistory[i].index)->SoundRef;
}
audDisplayf("History %d: %s (%s track %u)", i, SOUNDFACTORY.GetMetadataManager().GetObjectName(soundHash), unpackedCategory >= NUM_RADIO_TRACK_CATS ? "Invalid" : TrackCats_ToString((TrackCats)unpackedCategory), data.trackHistory[i].index);
#endif
}
}
history->SetWriteIndex(1);
// set up the current track
category = (*history)[0] & 0xffff;
index = (*history)[0] >> 16;
}
if (GetNameHash() == ATSTRINGHASH("RADIO_22_DLC_BATTLE_MIX1_RADIO", 0xF8BEAA16))
{
const audRadioStationTrackListCollection* trackList = FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC);
if (trackList)
{
u32 maxTrackIndex = Max(index, (u32)data.nextTrack.index);
u32 numTracks = trackList->ComputeNumTracks();
// Its possible that we get a sync request before the tuneables are loaded to unlock the station, so we need to unlock the appropriate tracks if we currently have them locked
if (maxTrackIndex >= numTracks)
{
audDisplayf("Syncing RADIO_22_DLC_BATTLE_MIX1_RADIO, track list %u is beyond the number of available tracks (%u), unlocking BATTLE_MIX2_RADIO", maxTrackIndex, numTracks);
UnlockTrackList("BATTLE_MIX2_RADIO");
numTracks = trackList->ComputeNumTracks();
if (maxTrackIndex >= numTracks)
{
audDisplayf("Syncing RADIO_22_DLC_BATTLE_MIX1_RADIO, track list %u is beyond the number of available tracks (%u), unlocking BATTLE_MIX3_RADIO", maxTrackIndex, numTracks);
UnlockTrackList("BATTLE_MIX3_RADIO");
numTracks = trackList->ComputeNumTracks();
if (maxTrackIndex >= numTracks)
{
audDisplayf("Syncing RADIO_22_DLC_BATTLE_MIX1_RADIO, track list %u is beyond the number of available tracks (%u), unlocking BATTLE_MIX4_RADIO", maxTrackIndex, numTracks);
UnlockTrackList("BATTLE_MIX4_RADIO");
}
}
}
}
sm_HasSyncedUnlockableDJStation = true;
}
// Sequential music stations should just store track index directly
if(m_PlaySequentialMusicTracks)
{
(*(const_cast<audRadioStationHistory*>(FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC))))[0] = index;
}
if(category < NUM_RADIO_TRACK_CATS)
{
const RadioStationTrackList::tTrack *trackMetadata = GetTrack(category, kNullContext, index);
if(trackMetadata)
{
s32 desiredTracKPlaytimeMS = (s32)(data.currentTrackPlaytime * m_Tracks[m_ActiveTrackIndex].GetDuration());
// If we're already playing the desired track and we're within a certain error range of the desired playback point,
// then just keep it playing rather than shutting down and restarting (as this will cause an audible drop out)
if(m_Tracks[m_ActiveTrackIndex].GetSoundRef() == trackMetadata->SoundRef && Abs(m_Tracks[m_ActiveTrackIndex].GetPlayTime() - desiredTracKPlaytimeMS) < 5000)
{
u32 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
m_Tracks[nextTrackIndex].Shutdown();
audDisplayf("Station %s is already synced to the active track - ignoring sync data", GetName());
}
else
{
m_ActiveTrackIndex = 0;
m_Tracks[0].Shutdown();
m_Tracks[1].Shutdown();
m_Tracks[1].Init(category, index, trackMetadata->SoundRef, data.currentTrackPlaytime, m_IsMixStation && !IsUSBMixStation(), m_IsReverbStation, m_NameHash);
audAssertf(m_Tracks[1].GetDuration() > 0, "Invalid track/sound metadata: %s (%s : %u)", GetName(), GetTrackCategoryName(category), index);
}
}
else
{
audWarningf("%s - Failed to sync current network radio track %u / %u", GetName(), category, index);
}
}
else
{
audWarningf("%s - Failed to sync current network radio track; invalid category %u / %u", GetName(), category, index);
}
PrepareNextTrack();
m_NetworkNextTrack = data.nextTrack;
m_NetworkNextTrack.category = static_cast<u8>(UnpackCategoryFromNetwork(data.nextTrack.category));
#if __BANK
u32 soundHash = g_NullSoundHash;
const audRadioStationTrackListCollection* trackList = UnpackCategoryFromNetwork(data.nextTrack.category) < NUM_RADIO_TRACK_CATS ? FindTrackListsForCategory(m_NetworkNextTrack.category) : NULL;
if(trackList && m_NetworkNextTrack.index < trackList->ComputeNumTracks())
{
soundHash = trackList->GetTrack(m_NetworkNextTrack.index)->SoundRef;
}
audDisplayf("Next Track: %s (%s track %u)", SOUNDFACTORY.GetMetadataManager().GetObjectName(soundHash), category >= NUM_RADIO_TRACK_CATS ? "Invalid" : TrackCats_ToString((TrackCats)m_NetworkNextTrack.category), m_NetworkNextTrack.index);
#endif
audDisplayf("Random Seed: %u", data.randomSeed);
audDisplayf("Current Track Playtime: %f", data.currentTrackPlaytime);
if(m_NetworkNextTrack.category >= NUM_RADIO_TRACK_CATS)
{
// remote host hadn't picked a track when we grabbed the state
m_NetworkNextTrack.category = 0xff;
}
else if(!IsUSBMixStation())
{
ASSERT_ONLY(const RadioStationTrackList::tTrack *nextTrackPtr = GetTrack(m_NetworkNextTrack.category, kNullContext, m_NetworkNextTrack.index));
audAssertf(nextTrackPtr, "%s - Failed to find next track (%u / %s, %u)", GetName(), m_NetworkNextTrack.category, TrackCats_ToString((TrackCats)m_NetworkNextTrack.category), m_NetworkNextTrack.index);
}
// TODO: track valid selection times?
// TODO: deal with sequential music stations if we have any
}
void audRadioStation::PopulateSyncData(audRadioStationSyncData &syncData) const
{
audDisplayf("Populating radio station sync data for station %s (Local Player: %s, Host: %s, Synced Time: %u):", GetName(), NetworkInterface::GetLocalGamerName(), NetworkInterface::GetHostName(), NetworkInterface::GetSyncedTimeInMilliseconds());
syncData.nameHash = GetNameHash();
if(m_PlaySequentialMusicTracks)
{
// we want the sync history index 0 to be the currently playing track
// Sequential stations only ever use history slot 0, and it'll actually contain the index of the *next* track due
// to play at this point, which is of no use - instead, just query the track index directly from the track object itself
syncData.trackHistory[0].category = static_cast<u8>(PackCategoryForNetwork(m_Tracks[m_ActiveTrackIndex].GetCategory()));
syncData.trackHistory[0].index = static_cast<u8>(m_Tracks[m_ActiveTrackIndex].GetTrackIndex());
#if __BANK
u32 soundHash = g_NullSoundHash;
const audRadioStationTrackListCollection* trackList = m_Tracks[m_ActiveTrackIndex].GetCategory() < NUM_RADIO_TRACK_CATS ? FindTrackListsForCategory(m_Tracks[m_ActiveTrackIndex].GetCategory()) : NULL;
if(trackList && m_Tracks[m_ActiveTrackIndex].GetTrackIndex() < trackList->ComputeNumTracks())
{
soundHash = trackList->GetTrack(m_Tracks[m_ActiveTrackIndex].GetTrackIndex())->SoundRef;
}
audDisplayf("History %d: %s (%s track %u)", 0, SOUNDFACTORY.GetMetadataManager().GetObjectName(soundHash), m_Tracks[m_ActiveTrackIndex].GetCategory() >= NUM_RADIO_TRACK_CATS ? "Invalid" : TrackCats_ToString((TrackCats)m_Tracks[m_ActiveTrackIndex].GetCategory()), m_Tracks[m_ActiveTrackIndex].GetTrackIndex());
#endif
// Just populate the rest of the history with 'invalid'
u32 trackId = 0xffffffff;
const u32 category = static_cast<u32>(trackId & 0xffff);
trackId = trackId >> 16;
for(s32 index = 1; index < audRadioStationHistory::kRadioHistoryLength; index++)
{
syncData.trackHistory[index].category = static_cast<u8>(PackCategoryForNetwork(category));
syncData.trackHistory[index].index = static_cast<u8>(trackId);
}
}
else if (m_UseRandomizedStrideSelection)
{
u32 historyIndex = 0;
syncData.trackHistory[historyIndex].category = static_cast<u8>(PackCategoryForNetwork(m_Tracks[m_ActiveTrackIndex].GetCategory()));
syncData.trackHistory[historyIndex++].index = static_cast<u8>(m_Tracks[m_ActiveTrackIndex].GetTrackIndex());
audRadioStationPackedInt packedAccumulatedPlayTime;
audRadioStationPackedInt packedLastTakeoverTime;
packedAccumulatedPlayTime.m_Value = m_StationAccumulatedPlayTimeMs;
packedLastTakeoverTime.m_Value = m_TakeoverLastTimeMs;
// Takeover station requires syncing some extra data, but we don't need to sync every category so we can re-purpose some history slots
// We've got 8 bits per index, so pack our u32s across a set of history slots
// We've got 4 bits per category, so store our u8s in these
syncData.trackHistory[historyIndex].index = packedAccumulatedPlayTime.PackedData.m_Byte0;
Assign(syncData.trackHistory[historyIndex++].category, m_MusicRunningCount);
syncData.trackHistory[historyIndex].index = packedAccumulatedPlayTime.PackedData.m_Byte1;
syncData.trackHistory[historyIndex++].category = m_TakeoverMusicTrackCounter;
syncData.trackHistory[historyIndex].index = packedAccumulatedPlayTime.PackedData.m_Byte2;
syncData.trackHistory[historyIndex++].category = (u8)m_CategoryStride[RADIO_TRACK_CAT_MUSIC] + 8;
syncData.trackHistory[historyIndex].index = packedAccumulatedPlayTime.PackedData.m_Byte3;
syncData.trackHistory[historyIndex++].category = (u8)m_CategoryStride[RADIO_TRACK_CAT_IDENTS] + 8;
syncData.trackHistory[historyIndex].index = packedLastTakeoverTime.PackedData.m_Byte0;
syncData.trackHistory[historyIndex++].category = (u8)m_CategoryStride[RADIO_TRACK_CAT_TAKEOVER_DJSOLO] + 8;
syncData.trackHistory[historyIndex].index = packedLastTakeoverTime.PackedData.m_Byte1;
syncData.trackHistory[historyIndex++].category = (u8)m_CategoryStride[RADIO_TRACK_CAT_TAKEOVER_IDENTS] + 8;
syncData.trackHistory[historyIndex].index = packedLastTakeoverTime.PackedData.m_Byte2;
syncData.trackHistory[historyIndex++].category = (u8)m_CategoryStride[RADIO_TRACK_CAT_TAKEOVER_MUSIC] + 8;
syncData.trackHistory[historyIndex].index = packedLastTakeoverTime.PackedData.m_Byte3;
syncData.trackHistory[historyIndex++].category = (u8)m_CategoryStride[RADIO_TRACK_CAT_DJSOLO] + 8;
audDisplayf("Accumulated Play Time: %u", m_StationAccumulatedPlayTimeMs);
audDisplayf("Last Takeover Time Time: %u", m_TakeoverLastTimeMs);
audDisplayf("Takeover Music Track Count: %u", m_TakeoverMusicTrackCounter);
audDisplayf("Music Run Count: %u", m_MusicRunningCount);
for (u32 category = 0; category < TRACKCATS_MAX; category++)
{
if (IsRandomizedStrideCategory(category))
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(category));
syncData.trackHistory[historyIndex].category = static_cast<u8>(PackCategoryForNetwork(category));
syncData.trackHistory[historyIndex].index = static_cast<u8>(history ? history->GetPreviousEntry() : 0xff);
historyIndex++;
}
}
// Just populate the rest of the history with 'invalid'
u32 trackId = 0xffffffff;
const u32 category = static_cast<u32>(trackId & 0xffff);
trackId = trackId >> 16;
for (; historyIndex < audRadioStationHistory::kRadioHistoryLength; historyIndex++)
{
syncData.trackHistory[historyIndex].category = static_cast<u8>(PackCategoryForNetwork(category));
syncData.trackHistory[historyIndex].index = static_cast<u8>(trackId);
}
}
else
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC));
if(history)
{
for(s32 index = 0; index < audRadioStationHistory::kRadioHistoryLength; index++)
{
// we want the sync history index 0 to be the currently playing track, ie the previous write index
const u32 i = (index + history->GetWriteIndex() + audRadioStationHistory::kRadioHistoryLength - 1) % audRadioStationHistory::kRadioHistoryLength;
u32 trackId = (*history)[i];
const u32 category = static_cast<u32>(trackId & 0xffff);
trackId = trackId >> 16;
syncData.trackHistory[index].category = static_cast<u8>(PackCategoryForNetwork(category));
syncData.trackHistory[index].index = static_cast<u8>(trackId);
#if __BANK
u32 soundHash = g_NullSoundHash;
const audRadioStationTrackListCollection* trackList = category < NUM_RADIO_TRACK_CATS? FindTrackListsForCategory(category) : NULL;
if(trackList && trackId < trackList->ComputeNumTracks())
{
soundHash = trackList->GetTrack(trackId)->SoundRef;
}
audDisplayf("History %d: %s (%s track %u)", index, SOUNDFACTORY.GetMetadataManager().GetObjectName(soundHash), category >= NUM_RADIO_TRACK_CATS ? "Invalid" : TrackCats_ToString((TrackCats)category), trackId);
#endif
// we will generate sync asserts if category or index are invalid
// if these errors are generated, we may need to increase the number of bits
if(static_cast<unsigned>(syncData.trackHistory[index].category) > audRadioStationSyncData::TrackId::CATEGORY_MASK)
{
audErrorf("%d has invalid category value of %u. Clamping to %u", index, syncData.trackHistory[index].category, audRadioStationSyncData::TrackId::CATEGORY_MASK);
syncData.trackHistory[index].category = audRadioStationSyncData::TrackId::CATEGORY_MASK;
}
if(static_cast<unsigned>(syncData.trackHistory[index].index) > audRadioStationSyncData::TrackId::INDEX_MASK)
{
audErrorf("%d has invalid index value of %u. Clamping to %u", index, syncData.trackHistory[index].index, audRadioStationSyncData::TrackId::INDEX_MASK);
syncData.trackHistory[index].index = audRadioStationSyncData::TrackId::INDEX_MASK;
}
}
}
else
{
audDisplayf("No history");
}
}
u32 nextTrackIndex = (m_ActiveTrackIndex+1)&1;
syncData.nextTrack.category = static_cast<u8>(PackCategoryForNetwork(m_Tracks[nextTrackIndex].GetCategory()));
syncData.nextTrack.index = static_cast<u8>(m_Tracks[nextTrackIndex].GetTrackIndex());
#if __BANK
u32 soundHash = g_NullSoundHash;
const audRadioStationTrackListCollection* trackList = m_Tracks[nextTrackIndex].GetCategory() < NUM_RADIO_TRACK_CATS ? FindTrackListsForCategory(m_Tracks[nextTrackIndex].GetCategory()) : NULL;
if(trackList && m_Tracks[nextTrackIndex].GetTrackIndex() < trackList->ComputeNumTracks())
{
soundHash = trackList->GetTrack(m_Tracks[nextTrackIndex].GetTrackIndex())->SoundRef;
}
audDisplayf("Next Track: %s (%s track %u)", SOUNDFACTORY.GetMetadataManager().GetObjectName(soundHash), m_Tracks[nextTrackIndex].GetCategory() >= NUM_RADIO_TRACK_CATS ? "Invalid" : TrackCats_ToString((TrackCats)m_Tracks[nextTrackIndex].GetCategory()), m_Tracks[nextTrackIndex].GetTrackIndex());
#endif
syncData.randomSeed = m_Random.GetSeed();
audDisplayf("Random Seed: %u", syncData.randomSeed);
const u32 trackDuration = m_Tracks[m_ActiveTrackIndex].GetDuration();
syncData.currentTrackPlaytime = trackDuration > 0 ? Clamp(m_Tracks[m_ActiveTrackIndex].GetPlayTime() / (float)(trackDuration), 0.f, 1.f) : 0.f;
audDisplayf("Current Track Playtime: %f", syncData.currentTrackPlaytime);
}
void audRadioStation::Reset(void)
{
StopRetuneSounds();
if(sm_DjSpeechMonoSound)
{
sm_DjSpeechMonoSound->StopAndForget();
sm_DjSpeechMonoSound = NULL;
}
if(sm_DjSpeechPositionedSound)
{
sm_DjSpeechPositionedSound->StopAndForget();
sm_DjSpeechPositionedSound = NULL;
}
for(u32 i = 0; i < sm_NumRadioStations; i++)
{
GetStation(i)->Shutdown();
}
}
audRadioStation *audRadioStation::FindStation(const char *name)
{
return FindStation(atStringHash(name));
}
audRadioStation *audRadioStation::FindStation(u32 nameHash)
{
s32 stationIndex = FindIndexOfStationWithHash(nameHash);
if (stationIndex >= 0)
{
return sm_OrderedRadioStations[stationIndex];
}
return NULL;
}
u32 audRadioStation::GetStationIndex() const
{
for(s32 stationIndex=0; stationIndex<sm_NumRadioStations; stationIndex++)
{
if(sm_OrderedRadioStations[stationIndex] == this)
{
return stationIndex;
}
}
return g_OffRadioStation;
}
s32 audRadioStation::GetStationImmutableIndex(audRadioStationImmutableIndexType indexType) const
{
return m_StationImmutableIndex[indexType];
}
s32 audRadioStation::FindIndexOfStationWithHash(u32 nameHash)
{
for(s32 stationIndex=0; stationIndex<sm_NumRadioStations; stationIndex++)
{
if(sm_OrderedRadioStations[stationIndex] && (sm_OrderedRadioStations[stationIndex]->GetNameHash() == nameHash))
{
return stationIndex;
}
}
return -1;
}
audRadioStation *audRadioStation::GetStation(u32 index)
{
return ((index < sm_NumRadioStations) ? sm_OrderedRadioStations[index] : NULL);
}
audRadioStation *audRadioStation::FindStationWithImmutableIndex(u32 index, audRadioStationImmutableIndexType indexType)
{
for(u32 stationIndex = 0; stationIndex < sm_NumRadioStations; stationIndex++)
{
audRadioStation* radioStation = sm_OrderedRadioStations[stationIndex];
if(radioStation && radioStation->m_StationImmutableIndex[indexType] == (s32)index)
{
return radioStation;
}
}
return nullptr;
}
void audRadioStation::StartEndCredits()
{
sm_PlayingEndCredits = true;
}
void audRadioStation::StopEndCredits()
{
sm_PlayingEndCredits = false;
}
void audRadioStation::SetHidden(bool isHidden)
{
AUD_CLEAR_TRISTATE_VALUE(const_cast<RadioStationSettings*>(m_StationSettings)->Flags, FLAG_ID_RADIOSTATIONSETTINGS_HIDDEN);
AUD_SET_TRISTATE_VALUE(const_cast<RadioStationSettings*>(m_StationSettings)->Flags, FLAG_ID_RADIOSTATIONSETTINGS_HIDDEN, isHidden ? AUD_TRISTATE_TRUE : AUD_TRISTATE_FALSE);
}
bool audRadioStation::IsFavourited() const
{
return m_IsFavourited || !CNetwork::IsGameInProgress();
}
void audRadioStation::SetLocked(const bool isLocked)
{
if(!isLocked && AUD_GET_TRISTATE_VALUE(m_StationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_LOCKED) == AUD_TRISTATE_TRUE)
{
audDisplayf("Unlocking radio station %s", GetName());
const_cast<RadioStationSettings*>(m_StationSettings)->Flags &= ~(0x03 << (FLAG_ID_RADIOSTATIONSETTINGS_LOCKED * 2));
AUD_SET_TRISTATE_VALUE(const_cast<RadioStationSettings*>(m_StationSettings)->Flags, FLAG_ID_RADIOSTATIONSETTINGS_LOCKED, AUD_TRISTATE_FALSE);
FlagLockedStationChange();
}
else if(isLocked && AUD_GET_TRISTATE_VALUE(m_StationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_LOCKED) != AUD_TRISTATE_TRUE)
{
audDisplayf("Locking radio station %s", GetName());
const_cast<RadioStationSettings*>(m_StationSettings)->Flags &= ~(0x03 << (FLAG_ID_RADIOSTATIONSETTINGS_LOCKED * 2));
AUD_SET_TRISTATE_VALUE(const_cast<RadioStationSettings*>(m_StationSettings)->Flags, FLAG_ID_RADIOSTATIONSETTINGS_LOCKED, AUD_TRISTATE_TRUE);
FlagLockedStationChange();
}
}
void audRadioStation::SetFavourited(bool isFavourited)
{
if(isFavourited != m_IsFavourited)
{
// Any station that is able to be be set as a favorite station should be entered into the FIXED_ORDER_RADIO_STATION_HASH_LIST_TUNEABLE
// list, which is used as the index of a bitset by telemetry code to track favorite-ed stations. NB. if adding a station to the above
// list, it must be done at the end to ensure existing station indices are not altered.
audAssertf(!audNorthAudioEngine::GetObject<GameObjectHashList>(ATSTRINGHASH("FIXED_ORDER_RADIO_STATION_HASH_LIST_TUNEABLE", 0x616CE649)) || GetStationImmutableIndex(IMMUTABLE_INDEX_TUNEABLE) >= 0, "Non-tuneable station %s has no immutable index set up", GetName());
m_IsFavourited = isFavourited;
FlagLockedStationChange();
}
}
void audRadioStation::GetFavouritedStations(u32 &favourites1, u32 &favourites2)
{
favourites1 = 0;
favourites2 = 0;
for(u32 stationIndex = 0; stationIndex < sm_NumRadioStations; stationIndex++)
{
audRadioStation* radioStation = sm_OrderedRadioStations[stationIndex];
if(radioStation && radioStation->m_IsFavourited)
{
s32 immutableIndex = radioStation->m_StationImmutableIndex[IMMUTABLE_INDEX_TUNEABLE];
if(immutableIndex < 0) continue;
if(audVerifyf(immutableIndex < 64, "IMMUTABLE_INDEX_TUNEABLE index exceeds bitset size"))
{
if(immutableIndex < 32)
{
favourites1 |= (u32)(1u << immutableIndex);
}
else
{
favourites2 |= (u32)(1u << (immutableIndex - 32));
}
}
}
}
}
void audRadioStation::GameUpdate()
{
const Vec3V s_CountryTalkRadioEmitter(752.0f, 4900.f, 0.f);
const float s_CountryTalkRadioRadius = 4000.f;
const float s_CountryTalkRadioHysterisis = 65.f;
audRadioStation *talkStation1 = FindStation(ATSTRINGHASH("RADIO_05_TALK_01", 0x406F56EC));
audRadioStation *talkStation2 = FindStation(ATSTRINGHASH("RADIO_11_TALK_02", 0xE6598737));
bool isSpectating = false;
if(talkStation1 && talkStation2)
{
CPed *player = CGameWorld::FindLocalPlayer();
if (NetworkInterface::IsGameInProgress() && NetworkInterface::IsLocalPlayerOnSCTVTeam())
{
CPed* followplayer = CGameWorld::FindFollowPlayer();
if (followplayer)
{
player = followplayer;
isSpectating = true;
}
}
if(player)
{
const Vec3V playerPosition = player->GetTransform().GetPosition();
const float distToEmitter = Dist(playerPosition, s_CountryTalkRadioEmitter).Getf();
if(distToEmitter < s_CountryTalkRadioRadius)
{
if(distToEmitter < s_CountryTalkRadioRadius - s_CountryTalkRadioHysterisis)
{
if(talkStation2->IsLocked())
{
talkStation2->SetLocked(false);
audDisplayf("Unlocking %s due to player position (Player:%.02f, %.02f, %.02f) - distance %.02f. Spectating: %s", talkStation2->GetName(), playerPosition.GetXf(), playerPosition.GetYf(), playerPosition.GetZf(), distToEmitter, isSpectating? "true" : "false");
}
if(!talkStation1->IsLocked())
{
talkStation1->SetLocked(true);
audDisplayf("Locking %s due to player position (Player:%.02f, %.02f, %.02f) - distance %.02f. Spectating: %s", talkStation1->GetName(), playerPosition.GetXf(), playerPosition.GetYf(), playerPosition.GetZf(), distToEmitter, isSpectating? "true" : "false");
}
sm_CountryTalkRadioSignal = 1.f;
}
else
{
sm_CountryTalkRadioSignal = (s_CountryTalkRadioRadius - distToEmitter) / s_CountryTalkRadioHysterisis;
}
}
else if(distToEmitter > s_CountryTalkRadioRadius)
{
if(!talkStation2->IsLocked())
{
talkStation2->SetLocked(true);
audDisplayf("Locking %s due to player position (Player:%.02f, %.02f, %.02f) - distance %.02f. Spectating: %s", talkStation2->GetName(), playerPosition.GetXf(), playerPosition.GetYf(), playerPosition.GetZf(), distToEmitter, isSpectating? "true" : "false");
sm_CountryTalkRadioSignal = 0.f;
}
if(talkStation1->IsLocked())
{
talkStation1->SetLocked(false);
audDisplayf("Unlocking %s due to player position (Player:%.02f, %.02f, %.02f) - distance %.02f. Spectating: %s", talkStation1->GetName(), playerPosition.GetXf(), playerPosition.GetYf(), playerPosition.GetZf(), distToEmitter, isSpectating? "true" : "false");
}
}
}
f32 signalLevel = 1.f;
if(!g_AudioEngine.GetSoundManager().IsPaused(2) && !g_RadioAudioEntity.IsRetuningVehicleRadio() && !g_RadioAudioEntity.IsRadioFadedOut() && g_RadioAudioEntity.IsPlayerRadioActive())
{
if(g_RadioAudioEntity.GetPlayerRadioStationPendingRetune() == talkStation2)
{
signalLevel = sm_CountryTalkRadioSignal;
}
else if(g_RadioAudioEntity.GetPlayerRadioStationPendingRetune() == talkStation1)
{
signalLevel = 1.f - sm_CountryTalkRadioSignal;
}
signalLevel = Min(signalLevel, sm_ScriptGlobalRadioSignalLevel);
}
g_AudioEngine.GetEnvironment().GetMusicSubmix()->SetEffectParam(0, ATSTRINGHASH("signalLevel", 0xFAF72E9), signalLevel);
g_AudioEngine.GetEnvironment().GetMusicSubmix()->SetEffectParam(0, ATSTRINGHASH("Bypass", 0x10F3D95C), signalLevel < 1.f? false : true);
}
}
void audRadioStation::UpdateStations(u32 timeInMs)
{
#if RSG_PC
#if __BANK
if(g_RadioAudioEntity.sm_DebugUserMusic)
DebugDrawUserMusic();
#endif
audRadioStation *userStation = FindStation(ATSTRINGHASH("RADIO_19_USER", 0xF74D5272));
if(userStation)
{
userStation->SetLocked(!HasUserTracks() || PARAM_disableuserradio.Get());
}
sm_UserRadioTrackManager.UpdateScanningUI();
#endif // _WIN32PC
u32 numUnlocked = 0;
u32 numUnlockedFavourited = 0;
for(u32 index=0; index<sm_NumRadioStations; index++)
{
audRadioStation *station = GetStation(index);
if(station)
{
station->Update(timeInMs);
if(!station->IsLocked() && !station->IsHidden())
{
numUnlocked++;
if(station->IsFavourited())
{
numUnlockedFavourited++;
}
}
}
}
UpdateDjSpeech(timeInMs);
if(numUnlocked != sm_NumUnlockedRadioStations || numUnlockedFavourited != sm_NumUnlockedFavouritedStations || sm_UpdateLockedStations)
{
// Re-sort the list to ensure unlocked stations are first in the list.
audDisplayf("Number of unlocked radio stations changed; was %u (%u favourited) now %u (%u favourited)", sm_NumUnlockedRadioStations, sm_NumUnlockedFavouritedStations, numUnlocked, numUnlockedFavourited);
sm_OrderedRadioStations.QSort(0, sm_NumRadioStations, QSortRadioStationCompareFunc);
sm_NumUnlockedRadioStations = numUnlocked;
sm_NumUnlockedFavouritedStations = numUnlockedFavourited;
sm_UpdateLockedStations = false;
}
}
void audRadioStation::UpdateRetuneSounds(void)
{
#if GTA_REPLAY
if(CReplayMgr::IsEditModeActive())
{
StopRetuneSounds();
return;
}
#endif //GTA_REPLAY
//Ensure the retune sounds are active.
audSoundInitParams initParams;
initParams.TimerId = 2;
if(sm_RetuneMonoSound == NULL)
{
g_RadioAudioEntity.CreateAndPlaySound_Persistent(g_RadioAudioEntity.GetRadioSounds().Find(ATSTRINGHASH("RetuneFrontend", 0xC46D20A)),
&sm_RetuneMonoSound, &initParams);
}
if(sm_RetunePositionedSound == NULL)
{
g_RadioAudioEntity.CreateAndPlaySound_Persistent(g_RadioAudioEntity.GetRadioSounds().Find(ATSTRINGHASH("RetunePositioned", 0x1B5588EC)),
&sm_RetunePositionedSound, &initParams);
}
f32 playerVehicleInsideFactor = g_RadioAudioEntity.GetPlayerVehicleInsideFactor();
if(sm_RetuneMonoSound)
{
f32 volumeDbMono = 0.f;
// use full volume retune loop when in frontend/lobby radio
if(!g_RadioAudioEntity.IsMobilePhoneRadioActive())
{
f32 volumeLinearMono = Max<f32>(sm_VehicleRadioRiseVolumeCurve.CalculateValue(playerVehicleInsideFactor),
g_SilenceVolumeLin);
volumeDbMono = audDriverUtil::ComputeDbVolumeFromLinear(volumeLinearMono);
volumeDbMono += g_RadioAudioEntity.GetVolumeOffset();
}
sm_RetuneMonoSound->SetRequestedVolume(volumeDbMono);
}
if(sm_RetunePositionedSound)
{
f32 volumeLinearPos = Max<f32>(sm_VehicleRadioRiseVolumeCurve.CalculateValue(1.0f - playerVehicleInsideFactor),
g_SilenceVolumeLin);
f32 volumeDbPos = audDriverUtil::ComputeDbVolumeFromLinear(volumeLinearPos);
volumeDbPos += g_RadioAudioEntity.GetVolumeOffset();
sm_RetunePositionedSound->SetRequestedVolume(volumeDbPos);
CVehicle *lastPlayerVehicle = g_RadioAudioEntity.GetLastPlayerVehicle();
if(lastPlayerVehicle != NULL)
{
//Attach positioned retune sound to the current or last player vehicle.
Vector3 vehiclePosition = VEC3V_TO_VECTOR3(lastPlayerVehicle->GetTransform().GetPosition());
sm_RetunePositionedSound->SetRequestedPosition(vehiclePosition);
}
}
}
void audRadioStation::StopRetuneSounds(void)
{
if(sm_RetuneMonoSound)
{
sm_RetuneMonoSound->StopAndForget();
sm_RetuneMonoSound = NULL;
}
if(sm_RetunePositionedSound)
{
sm_RetunePositionedSound->StopAndForget();
sm_RetunePositionedSound = NULL;
}
}
void audRadioStation::SkipForwardStations(u32 timeToSkipMs)
{
for(u32 index=0; index<sm_NumRadioStations; index++)
{
audRadioStation *station = GetStation(index);
if(station && station->IsStreamingVirtually())
{
station->SkipForward(timeToSkipMs);
}
}
}
void audRadioStation::SkipForward(u32 timeToSkip)
{
if(IsFrozen())
{
audWarningf("%s ignoring skip request (%ums) due to being frozen", GetName(), timeToSkip);
}
else
{
audDisplayf("Radio station %s is skipping forward by %ums (current track: %u)", GetName(), timeToSkip, m_Tracks[m_ActiveTrackIndex].GetSoundRef());
u32 timeLeft = m_Tracks[m_ActiveTrackIndex].GetDuration() - (u32)m_Tracks[m_ActiveTrackIndex].GetPlayTime();
if(timeToSkip > timeLeft && !PARAM_disableradiohistory.Get())
{
// skip a random amount into the next track too
u32 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
if(m_Tracks[nextTrackIndex].IsInitialised())
{
u32 nextDuration = m_Tracks[nextTrackIndex].GetDuration();
u32 min = Min(10000U, nextDuration);
u32 max = Min(nextDuration, nextDuration - 20000U);
m_Tracks[nextTrackIndex].SkipForward((u32)GetRandomNumberInRange((s32)min, (s32)max));
}
}
m_Tracks[m_ActiveTrackIndex].SkipForward(timeToSkip);
}
}
#if RSG_PC // user music
void audRadioStation::NextTrack()
{
if(m_IsUserStation && !sm_IsInRecoveryMode && sm_UserRadioTrackManager.GetRadioMode() != USERRADIO_PLAY_MODE_RADIO)
{
if(m_Tracks[m_ActiveTrackIndex].IsInitialised() && m_Tracks[m_ActiveTrackIndex].GetPlayTime() > 2000)
{
sm_ForceUserNextTrack = true;
}
}
}
void audRadioStation::PrevTrack()
{
if(m_Tracks[m_ActiveTrackIndex].IsInitialised() && m_Tracks[m_ActiveTrackIndex].GetPlayTime() > 2000 && sm_UserRadioTrackManager.GetRadioMode() == USERRADIO_PLAY_MODE_SEQUENTIAL)
{
sm_ForceUserPrevTrack = true;
}
}
#endif
void audRadioStation::UpdateDjSpeech(u32 timeInMs)
{
const audRadioStation *playerStation = g_RadioAudioEntity.GetPlayerRadioStation();
if(sm_IMDrivenDjSpeech)
{
playerStation = sm_IMDjSpeechStation;
}
if(playerStation && (sm_IMDrivenDjSpeech || (playerStation->IsStreamingPhysically() && !g_RadioAudioEntity.IsRetuning() && !IsPlayingEndCredits() REPLAY_ONLY(&& !CReplayMgr::IsEditModeActive()))))
{
//Update DJ speech for the player's radio station.
s32 trackPlayTimeMs = sm_IMDrivenDjSpeech ? g_InteractiveMusicManager.GetCurPlayTimeMs() :
playerStation->GetCurrentPlayTimeOfActiveTrack();
bool isPlayingSpeech = false;
bool isDummySpeech = false;
u32 clientVar = 0;
if(sm_DjSpeechMonoSound)
{
sm_DjSpeechMonoSound->GetClientVariable(clientVar);
isDummySpeech |= (clientVar == 0xdeadbeef);
}
if(sm_DjSpeechPositionedSound)
{
sm_DjSpeechPositionedSound->GetClientVariable(clientVar);
isDummySpeech |= (clientVar == 0xdeadbeef);
}
f32 playerVehicleInsideFactor;
switch(sm_DjSpeechState)
{
case RADIO_DJ_SPEECH_STATE_PREPARING:
if(sm_DjSpeechMonoSound && (sm_IMDrivenDjSpeech || trackPlayTimeMs <= (sm_ActiveDjSpeechStartTimeMs + g_RadioDjPlayTimeTresholdMs)))
{
bool isPrepared = false;
switch(sm_DjSpeechMonoSound->Prepare(g_RadioDjSpeechWaveSlot, true))
{
case AUD_PREPARE_FAILED:
Warningf("Failed to prepare DJ speech for station %s", playerStation->GetName());
if(sm_DjSpeechMonoSound)
{
sm_DjSpeechMonoSound->StopAndForget();
}
if(sm_DjSpeechPositionedSound)
{
sm_DjSpeechPositionedSound->StopAndForget();
}
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_IDLE;
break;
case AUD_PREPARED:
isPrepared = true;
break;
case AUD_PREPARING:
default:
break;
}
if(isPrepared)
{
//Ensure that the positioned speech sound (with the same wave reference) is also prepared.
switch(sm_DjSpeechPositionedSound->Prepare(g_RadioDjSpeechWaveSlot, true))
{
case AUD_PREPARE_FAILED:
naWarningf("Failed to prepare DJ speech for station %s", playerStation->GetName());
if(sm_DjSpeechMonoSound)
{
sm_DjSpeechMonoSound->StopAndForget();
}
if(sm_DjSpeechPositionedSound)
{
sm_DjSpeechPositionedSound->StopAndForget();
}
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_IDLE;
break;
case AUD_PREPARED:
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_PREPARED;
break;
case AUD_PREPARING:
default:
break;
}
}
}
else
{
//We are out of time to prepare and play our DJ speech, so stop trying and cleanup.
naDisplayf("Failed to prepare Dj speech in time");
if(sm_DjSpeechMonoSound)
sm_DjSpeechMonoSound->StopAndForget();
if(sm_DjSpeechPositionedSound)
sm_DjSpeechPositionedSound->StopAndForget();
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_IDLE;
}
break;
case RADIO_DJ_SPEECH_STATE_PREPARED:
if(!sm_IMDrivenDjSpeech || sm_IMDrivenDjSpeechScheduled)
{
if(trackPlayTimeMs >= (sm_ActiveDjSpeechStartTimeMs - g_RadioDjPlayTimeTresholdMs))
{
bool isDJSpeechRetriggerTimeValid = true;
if(playerStation->GetNameHash() == ATSTRINGHASH("RADIO_37_MOTOMAMI", 0x97074FCC) && !isDummySpeech)
{
if(g_AudioEngine.GetSoundManager().GetTimeInMilliseconds(2) - playerStation->m_LastTimeDJSpeechPlaying < g_RadioDjSpeechMinDelayMsMotomami)
{
isDJSpeechRetriggerTimeValid = false;
}
}
const u32 nextTrackCategory = playerStation->GetNextTrack().GetCategory();
const u32 currentTrackCategory = playerStation->GetCurrentTrack().GetCategory();
if(isDJSpeechRetriggerTimeValid && trackPlayTimeMs <= (sm_ActiveDjSpeechStartTimeMs + g_RadioDjPlayTimeTresholdMs) && (sm_IMDrivenDjSpeech || (sm_DjSpeechStation == playerStation && sm_DjSpeechNextTrackCategory == nextTrackCategory && (currentTrackCategory == RADIO_TRACK_CAT_MUSIC || currentTrackCategory == RADIO_TRACK_CAT_TAKEOVER_MUSIC))))
{
if(sm_DjSpeechMonoSound)
{
sm_DjSpeechMonoSound->Play();
}
if(sm_DjSpeechPositionedSound)
{
sm_DjSpeechPositionedSound->Play();
}
const_cast<audRadioStation*>(playerStation)->AddActiveDjSpeechToHistory(timeInMs);
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_PLAYING;
sm_DjSpeechStation = NULL;
}
else
{
//We are out of time to play our DJ speech, so cleanup.
if(!sm_IMDrivenDjSpeech && sm_DjSpeechStation != playerStation)
{
audWarningf("Cancelled DJ speech due to station change; scheduled on %s now %s", sm_DjSpeechStation ? sm_DjSpeechStation->GetName() : "null", playerStation ? playerStation->GetName() : "null");
}
else if(!sm_IMDrivenDjSpeech && sm_DjSpeechNextTrackCategory != nextTrackCategory)
{
audWarningf("Cancelled DJ speech due to next track category change; scheduled for %s now %s", TrackCats_ToString((TrackCats)sm_DjSpeechNextTrackCategory), TrackCats_ToString((TrackCats)nextTrackCategory));
}
else if(!isDJSpeechRetriggerTimeValid)
{
audWarningf("Cancelled DJ speech due to triggering within min retrigger time");
}
else
{
audWarningf("Failed to play Dj speech in time");
}
if(sm_DjSpeechMonoSound)
{
sm_DjSpeechMonoSound->StopAndForget();
}
if(sm_DjSpeechPositionedSound)
{
sm_DjSpeechPositionedSound->StopAndForget();
}
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_IDLE;
break;
}
}
}
// intentional fall through if the speech is now playing so it doesn't go a frame without valid volume/filter
case RADIO_DJ_SPEECH_STATE_PLAYING:
//Update volumes (and position) of DJ speech sounds.
playerVehicleInsideFactor = g_RadioAudioEntity.GetPlayerVehicleInsideFactor();
if(sm_DjSpeechMonoSound)
{
isPlayingSpeech = true;
sm_DjSpeechMonoSound->SetRequestedVolume(sm_DjSpeechFrontendVolume);
sm_DjSpeechMonoSound->SetRequestedLPFCutoff(sm_DjSpeechFrontendLPF);
}
if(sm_DjSpeechPositionedSound)
{
isPlayingSpeech = true;
f32 volumeLinearPos = Max<f32>(sm_VehicleRadioRiseVolumeCurve.CalculateValue(1.0f - playerVehicleInsideFactor),
g_SilenceVolumeLin);
f32 volumeDbPos = audDriverUtil::ComputeDbVolumeFromLinear(volumeLinearPos);
volumeDbPos += g_RadioAudioEntity.GetVolumeOffset();
CVehicle *lastPlayerVehicle = g_RadioAudioEntity.GetLastPlayerVehicle();
if(lastPlayerVehicle != NULL)
{
//Attach positioned retune sound to the current or last player vehicle.
Vector3 vehiclePosition = VEC3V_TO_VECTOR3(lastPlayerVehicle->GetTransform().GetPosition());
sm_DjSpeechPositionedSound->SetRequestedPosition(vehiclePosition);
volumeDbPos += lastPlayerVehicle->GetVehicleAudioEntity()->GetAmbientRadioVolume();
}
else
{
// silence the dj if we dont have a vehicle to attach them to
volumeDbPos = -100.f;
}
// Prevent a harmless assert
volumeDbPos = Clamp(volumeDbPos + sm_PositionedPlayerVehicleRadioVolume, -100.f, 20.f);
sm_DjSpeechPositionedSound->SetRequestedVolume(0.f);
sm_DjSpeechPositionedSound->SetRequestedPostSubmixVolumeAttenuation(volumeDbPos);
sm_DjSpeechPositionedSound->SetRequestedHPFCutoff(sm_PositionedPlayerVehicleRadioHPFCutoff);
sm_DjSpeechPositionedSound->SetRequestedLPFCutoff(sm_PositionedPlayerVehicleRadioLPFCutoff);
sm_DjSpeechPositionedSound->SetRequestedEnvironmentalLoudnessFloat(sm_PositionedPlayerVehicleRadioEnvironmentalLoudness);
sm_DjSpeechPositionedSound->SetRequestedVolumeCurveScale(sm_PositionedPlayerVehicleRadioRollOff);
}
if(!isPlayingSpeech)
{
//The DJ speech has finished.
sm_IMDrivenDjSpeech = false;
sm_IMDrivenDjSpeechScheduled = false;
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_IDLE;
}
else if(!isDummySpeech && sm_DjSpeechState == RADIO_DJ_SPEECH_STATE_PLAYING)
{
const_cast<audRadioStation*>(playerStation)->m_LastTimeDJSpeechPlaying = g_AudioEngine.GetSoundManager().GetTimeInMilliseconds(2);
}
break;
case RADIO_DJ_SPEECH_STATE_IDLE: //Intentional fall-through.
default:
// Don't play DJ speech if we're playing a custom tracklist, rather than standard radio content
if(!sm_IMDrivenDjSpeech && trackPlayTimeMs >= 0 && !playerStation->HasQueuedTrackList() && playerStation->GetNextTrack().IsInitialised())
{
s32 introStartTimeMs = 0;
s32 introEndTimeMs = 0;
s32 outroStartTimeMs = 0;
s32 outroEndTimeMs = 0;
if(playerStation->ExtractDjMarkers(introStartTimeMs, introEndTimeMs, outroStartTimeMs, outroEndTimeMs))
{
//We found some active DJ markers, so check if they are within our time window.
u8 preferredDjSpeechCategory = RADIO_DJ_SPEECH_CAT_GENERAL;
s32 windowStartTimeMs = 0;
s32 windowLengthMs = -1;
s32 timeToWindowMs = introStartTimeMs - trackPlayTimeMs;
const u32 minPrepareTimeMs = playerStation->GetNameHash() == ATSTRINGHASH("RADIO_37_MOTOMAMI", 0x97074FCC) ? 500 : g_RadioDjMinPrepareTimeMs;
if(timeToWindowMs > minPrepareTimeMs)
{
windowStartTimeMs = introStartTimeMs;
windowLengthMs = introEndTimeMs - introStartTimeMs;
if(windowLengthMs > 0)
{
preferredDjSpeechCategory = RADIO_DJ_SPEECH_CAT_INTRO;
}
}
else
{
audDebugf1("DJSPEECH: Intro time to window too short: %d", timeToWindowMs);
}
// We want to block Outro speech before a DJSOLO on MOTOMAMI as we're pushing the outro regions to the end of the tracks, and the energy levels can be very different between general and
// DJSOLO
const bool allowOutroVO = !(playerStation->GetNameHash() == ATSTRINGHASH("RADIO_37_MOTOMAMI", 0x97074FCC) && playerStation->GetNextTrack().GetCategory() == RADIO_TRACK_CAT_DJSOLO);
timeToWindowMs = outroStartTimeMs - trackPlayTimeMs;
if(allowOutroVO && (windowLengthMs <= 0) && (timeToWindowMs > g_RadioDjMinPrepareTimeMs))
{
windowStartTimeMs = outroStartTimeMs;
windowLengthMs = outroEndTimeMs - outroStartTimeMs;
if(windowLengthMs > 0)
{
preferredDjSpeechCategory = RADIO_DJ_SPEECH_CAT_OUTRO;
}
}
if(windowLengthMs > 0)
{
//Decide if we want to attempt to play DJ speech at all.
const u32 nextTrackCategory = playerStation->GetNextTrack().GetCategory();
const bool isValidCategoryToSelect = timeInMs >= playerStation->m_DjSpeechCategoryNextValidSelectionTime[preferredDjSpeechCategory];
f32 djSpeechProbability = g_RadioDjSpeechProbability;
if (playerStation->m_NameHash == ATSTRINGHASH("RADIO_34_DLC_HEI4_KULT", 0xE3442163))
{
djSpeechProbability = g_RadioDjSpeechProbabilityKult;
}
else if (playerStation->GetNameHash() == ATSTRINGHASH("RADIO_37_MOTOMAMI", 0x97074FCC) && (preferredDjSpeechCategory == RADIO_DJ_SPEECH_CAT_OUTRO || preferredDjSpeechCategory == RADIO_DJ_SPEECH_CAT_GENERAL))
{
djSpeechProbability = g_RadioDjOutroSpeechProbabilityMotomami;
}
// always speak when going to news or weather
// always speak intros for takeover music
// NOTE: using global random stream for dj speech
if(BANK_ONLY(g_ForceDjSpeech || ) (playerStation->m_HasTakeoverContent && preferredDjSpeechCategory==RADIO_DJ_SPEECH_CAT_INTRO) || nextTrackCategory == RADIO_TRACK_CAT_WEATHER || nextTrackCategory == RADIO_TRACK_CAT_NEWS || (isValidCategoryToSelect && audEngineUtil::ResolveProbability(djSpeechProbability)))
{
u32 speechLengthMs = playerStation->CreateCompatibleDjSpeech(timeInMs, preferredDjSpeechCategory, windowLengthMs, windowStartTimeMs);
if((speechLengthMs > 0) && sm_DjSpeechMonoSound && sm_DjSpeechPositionedSound)
{
//Log the game time that this DJ speech is intended to be played at.
//NOTE: Align intro and outro speech to the end of the window.
sm_ActiveDjSpeechStartTimeMs = windowStartTimeMs + (windowLengthMs - (s32)speechLengthMs);
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_PREPARING;
sm_DjSpeechStation = playerStation;
sm_DjSpeechNextTrackCategory = nextTrackCategory;
audDebugf1("DJ Speech start time: %u (window %u, length %u, speechLength %u", sm_ActiveDjSpeechStartTimeMs, windowStartTimeMs, windowLengthMs, speechLengthMs);
}
}
else
{
//Play a dummy (wrapper) speech sound to prevent valid DJ speech from being selected.
audSoundInitParams initParams;
initParams.TimerId = 2;
initParams.u32ClientVar = 0xdeadbeef;
g_RadioAudioEntity.CreateSound_PersistentReference(g_RadioAudioEntity.GetRadioSounds().Find(ATSTRINGHASH("DjSpeechDummy", 0x94D36E43)),
&sm_DjSpeechMonoSound, &initParams);
if(sm_DjSpeechMonoSound)
{
g_RadioAudioEntity.CreateSound_PersistentReference(g_RadioAudioEntity.GetRadioSounds().Find(ATSTRINGHASH("DjSpeechDummy", 0x94D36E43)),
&sm_DjSpeechPositionedSound, &initParams);
if(sm_DjSpeechPositionedSound)
{
sm_ActiveDjSpeechStartTimeMs = windowStartTimeMs + windowLengthMs;
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_PREPARING;
sm_DjSpeechStation = playerStation;
sm_DjSpeechNextTrackCategory = nextTrackCategory;
}
else
{
sm_DjSpeechMonoSound->StopAndForget();
}
}
}
}
}
}
break;
}
}
else
{
//The player is not listening to a radio station, so clean up DJ speech.
if(sm_DjSpeechMonoSound)
{
sm_DjSpeechMonoSound->StopAndForget();
}
if(sm_DjSpeechPositionedSound)
{
sm_DjSpeechPositionedSound->StopAndForget();
}
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_IDLE;
}
}
audRadioStation::audRadioStation(RadioStationSettings *stationSettings) :
m_StationSettings(stationSettings),
m_ForceNextTrackIndex(0xffff),
m_ForceNextTrackCategory(0xff),
m_ForceNextTrackContext(kNullContext),
m_ActiveTrackIndex(0),
m_DjSpeechHistoryWriteIndex(0),
m_SoundBucketId(0xff),
m_ShouldStreamPhysically(false),
m_IsPlayingOverlappedTrack(false),
m_IsPlayingMixTransition(false),
m_IsRequestingOverlappedTrackPrepare(false),
m_HasJustPlayedBackToBackMusic(false),
m_ShouldPlayFullRadio(false),
m_ScriptSetMusicOnly(false),
m_IsFirstMusicTrackSinceBoot(true),
m_IsMixStation(false),
m_IsFavourited(true),
m_IsReverbStation(false),
m_HasConstructedMixStationBeatMap(false)
{
for(u32 trackIndex = 0; trackIndex < 2; trackIndex++)
{
m_WaveSlots[trackIndex] = NULL;
}
m_NameHash = atStringHash(m_StationSettings->Name);
audAssertf(m_NameHash == atStringHash(audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(m_StationSettings->NameTableOffset)), "RadioStationSettings %s's name field (%s) does not match the gameobject name", audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(m_StationSettings->NameTableOffset), m_StationSettings->Name);
for(u32 i = 0; i < IMMUTABLE_INDEX_TYPE_MAX; i++)
{
m_StationImmutableIndex[i] = -1;
}
if(!IsHidden())
{
if(GameObjectHashList* tuneableStationHashList = audNorthAudioEngine::GetObject<GameObjectHashList>(ATSTRINGHASH("FIXED_ORDER_RADIO_STATION_HASH_LIST_TUNEABLE", 0x616CE649)))
{
for(u32 i = 0; i < tuneableStationHashList->numGameObjectHashes; i++)
{
if(tuneableStationHashList->GameObjectHashes[i].GameObjectHash == m_NameHash)
{
m_StationImmutableIndex[IMMUTABLE_INDEX_TUNEABLE] = i;
}
}
audAssertf(m_StationImmutableIndex[IMMUTABLE_INDEX_TUNEABLE] >= 0, "Non-hidden station %s has not been assigned an immutable index - please add it *at the end*<-(IMPORTANT!) of FIXED_ORDER_RADIO_STATION_HASH_LIST_TUNEABLE", GetName());
}
}
if(GameObjectHashList* stationHashList = audNorthAudioEngine::GetObject<GameObjectHashList>(ATSTRINGHASH("FIXED_ORDER_RADIO_STATION_HASH_LIST_GLOBAL", 0xEE10457C)))
{
for(u32 i = 0; i < stationHashList->numGameObjectHashes; i++)
{
if(stationHashList->GameObjectHashes[i].GameObjectHash == m_NameHash)
{
m_StationImmutableIndex[IMMUTABLE_INDEX_GLOBAL] = i;
}
}
audAssertf(m_StationImmutableIndex[IMMUTABLE_INDEX_GLOBAL] >= 0, "Station %s has not been assigned a global immutable index - please add it *at the end*<-(IMPORTANT!) of FIXED_ORDER_RADIO_STATION_HASH_LIST_GLOBAL", GetName());
}
for(u32 djHistoryIndex=0; djHistoryIndex<g_MaxDjSpeechInHistory; djHistoryIndex++)
{
m_DjSpeechHistory[djHistoryIndex] = 0;
}
//Randomly distribute the valid start times for each category across the stations.
for(u32 djCatIndex=0; djCatIndex<NUM_RADIO_DJ_SPEECH_CATS; djCatIndex++)
{
// NOTE: using global random stream for DJ Speech
const u32 repeatTime = (m_NameHash == ATSTRINGHASH("RADIO_34_DLC_HEI4_KULT", 0xE3442163)) ? g_RadioDjSpeechMinTimeBetweenRepeatedCategoriesKult[djCatIndex] : g_RadioDjSpeechMinTimeBetweenRepeatedCategories[djCatIndex];
m_DjSpeechCategoryNextValidSelectionTime[djCatIndex] = static_cast<u32>(audEngineUtil::GetRandomNumberInRange(static_cast<s32>(g_AudioEngine.GetSoundManager().GetTimeInMilliseconds(2)),
static_cast<s32>(repeatTime)));
}
m_NoBackToBackMusic = (AUD_GET_TRISTATE_VALUE(stationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_NOBACK2BACKMUSIC) == AUD_TRISTATE_TRUE);
m_PlaysBackToBackAds = (AUD_GET_TRISTATE_VALUE(stationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_BACK2BACKADS) == AUD_TRISTATE_TRUE);
m_PlayWeather = (AUD_GET_TRISTATE_VALUE(stationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_PLAYWEATHER) == AUD_TRISTATE_TRUE);
m_PlayNews = (AUD_GET_TRISTATE_VALUE(stationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_PLAYNEWS) == AUD_TRISTATE_TRUE);
m_PlaySequentialMusicTracks = (AUD_GET_TRISTATE_VALUE(stationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_SEQUENTIALMUSIC) == AUD_TRISTATE_TRUE);
m_PlayIdentsInsteadOfAds = (AUD_GET_TRISTATE_VALUE(stationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_IDENTSINSTEADOFADS) == AUD_TRISTATE_TRUE);
m_IsFrozen = false;
m_IsFirstTrack = true;
m_HasTakeoverContent = false;
m_UseRandomizedStrideSelection = false;
m_LastTimeDJSpeechPlaying = 0u;
m_AmbientVolume = 0.0f;
WIN32PC_ONLY(m_IsUserStation = AUD_GET_TRISTATE_VALUE(stationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_PLAYSUSERSMUSIC) == AUD_TRISTATE_TRUE);
}
audRadioStation::~audRadioStation()
{
for(u32 trackIndex = 0; trackIndex < 2; trackIndex++)
{
m_Tracks[trackIndex].Shutdown();
}
}
void audRadioStation::Init()
{
const RadioStationSettings *stationSettings = m_StationSettings;
m_Random.Reset(audEngineUtil::GetRandomInteger());
// Start by assuming we don't have a full set of content
m_OnlyPlayAds = true;
for(s32 i = 0; i < NUM_RADIO_TRACK_CATS; i++)
{
//Randomly distribute the valid start times for each category across the stations.
const s32 nextValidSelectionTime = GetRandomNumberInRange(static_cast<s32>(g_AudioEngine.GetSoundManager().GetTimeInMilliseconds(2)),
static_cast<s32>(sm_MinTimeBetweenRepeatedCategories->Category[i].Value));
m_NextValidSelectionTime[i] = static_cast<u32>(nextValidSelectionTime);
m_CategoryStride[i] = 0;
}
m_DLCInitialized = false;
m_LastForcedMusicTrackList = 0u;
for(u32 trackListIndex = 0; trackListIndex < stationSettings->numTrackLists; trackListIndex++)
{
RadioStationTrackList *trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(stationSettings->TrackList[trackListIndex].TrackListRef);
if(trackList)
{
if (trackList->Category == RADIO_TRACK_CAT_MUSIC)
{
AddMusicTrackList(trackList, stationSettings->TrackList[trackListIndex].TrackListRef);
}
else
{
AddTrackList(trackList);
}
if (trackList->Category != RADIO_TRACK_CAT_MUSIC)
{
if(sm_IsNetworkModeHistoryActive)
{
m_HasTakeoverContent |= (trackList->Category == RADIO_TRACK_CAT_TAKEOVER_MUSIC);
}
}
}
}
if (m_HasTakeoverContent)
{
m_UseRandomizedStrideSelection = true;
}
if(m_NameHash == ATSTRINGHASH("RADIO_37_MOTOMAMI", 0x97074FCC) ||
m_NameHash == ATSTRINGHASH("RADIO_03_HIPHOP_NEW", 0xFA17DE37) ||
m_NameHash == ATSTRINGHASH("RADIO_09_HIPHOP_OLD", 0x572C04))
{
m_UseRandomizedStrideSelection = true;
}
if(m_UseRandomizedStrideSelection)
{
RandomizeCategoryStrides(true, false);
}
if (m_NameHash == ATSTRINGHASH("RADIO_22_DLC_BATTLE_MIX1_CLUB", 0x7E78FD4E) ||
m_NameHash == ATSTRINGHASH("RADIO_23_DLC_BATTLE_MIX2_CLUB", 0x1FDBE6BE) ||
m_NameHash == ATSTRINGHASH("RADIO_24_DLC_BATTLE_MIX3_CLUB", 0xB48221E3) ||
m_NameHash == ATSTRINGHASH("RADIO_25_DLC_BATTLE_MIX4_CLUB", 0x7E95A6DD) ||
m_NameHash == ATSTRINGHASH("DLC_BATTLE_MIX1_CLUB_PRIV", 0x34BF5EC8) ||
m_NameHash == ATSTRINGHASH("DLC_BATTLE_MIX2_CLUB_PRIV", 0x6703639B) ||
m_NameHash == ATSTRINGHASH("DLC_BATTLE_MIX3_CLUB_PRIV", 0x841DDC99) ||
m_NameHash == ATSTRINGHASH("DLC_BATTLE_MIX4_CLUB_PRIV", 0xEC2974F9))
{
m_IsMixStation = true;
}
if(m_NameHash == ATSTRINGHASH("RADIO_03_HIPHOP_NEW", 0xFA17DE37))
{
m_NewTrackBias = 1.2f;
m_FirstBootNewTrackBias = 2.f;
}
else
{
m_NewTrackBias = 1.f;
m_FirstBootNewTrackBias = 1.f;
}
m_IsMixStation |= AUD_GET_TRISTATE_VALUE(m_StationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_ISMIXSTATION) == AUD_TRISTATE_TRUE;
if (m_NameHash == ATSTRINGHASH("RADIO_30_DLC_HEI4_MIX1_REVERB", 0xCEE7651B))
{
m_IsReverbStation = true;
}
m_TakeoverMusicTrackCounter = 0;
// Load tuning for iFruit station from Cloud Tunables to allow late tweaking
if(m_NameHash == ATSTRINGHASH("RADIO_23_DLC_XM19_RADIO", 0x6D1C6C45))
{
m_TakeoverProbability = 0.4f;
m_TakeoverMinTimeMs = 9 * 60000;
m_TakeoverLastTimeMs = 0;
m_TakeoverMinTracks = 3;
m_TakeoverMaxTracks = 4;
m_TakeoverDjSoloProbability = 0.55f;
if (Tunables::GetInstance().Access(BASE_GLOBALS_HASH, ATSTRINGHASH("RADIO_23_TAKEOVER_PROB", 0xD0D6E614), m_TakeoverProbability))
{
audDisplayf("RADIO_23_DLC_XM19_RADIO: Tunable defined TakeOver Probability: %f", m_TakeoverProbability);
}
if (Tunables::GetInstance().Access(BASE_GLOBALS_HASH, ATSTRINGHASH("RADIO_23_TAKEOVER_MINTIME", 0xF374152), m_TakeoverMinTimeMs))
{
audDisplayf("RADIO_23_DLC_XM19_RADIO: Tunable defined TakeOver MinTime: %u", m_TakeoverMinTimeMs);
}
if (Tunables::GetInstance().Access(BASE_GLOBALS_HASH, ATSTRINGHASH("RADIO_23_TAKEOVER_MINTRACKS", 0x5273ED4E), m_TakeoverMinTracks))
{
audDisplayf("RADIO_23_DLC_XM19_RADIO: Tunable defined TakeOver MinTracks: %u", m_TakeoverMinTracks);
}
if (Tunables::GetInstance().Access(BASE_GLOBALS_HASH, ATSTRINGHASH("RADIO_23_TAKEOVER_MAXTRACKS", 0x5F217590), m_TakeoverMaxTracks))
{
audDisplayf("RADIO_23_DLC_XM19_RADIO: Tunable defined TakeOver MaxTracks: %u", m_TakeoverMaxTracks);
}
if (Tunables::GetInstance().Access(BASE_GLOBALS_HASH, ATSTRINGHASH("RADIO_23_TAKEOVER_DJSOLO_PROB", 0x6A820BD6), m_TakeoverDjSoloProbability))
{
audDisplayf("RADIO_23_DLC_XM19_RADIO: Tunable defined TakeOver DjSolo Probability: %f", m_TakeoverDjSoloProbability);
}
}
// Initialise as if we had a takeover in recent history, so we don't rule out hearing one in the first 10 minutes
m_StationAccumulatedPlayTimeMs = GetRandomNumberInRange(0, m_TakeoverMinTimeMs);
m_ListenTimer = 0.f;
m_Genre = (RadioGenre)m_StationSettings->Genre;
m_AmbientVolume = (f32)m_StationSettings->AmbientRadioVol;
m_QueuedTrackList = NULL;
m_QueuedTrackListIndex = 0;
m_NetworkNextTrack.category = 0xff;
#if RSG_PC // user music
m_LastPlayedTrackIndex = ~0U;
m_CurrentlyPlayingTrackIndex = ~0U;
m_BadTrackCount = 0;
sm_IsInRecoveryMode = false;
#endif
m_FirstTrackStartOffset = GetRandomNumberInRange(0.1f, 0.8f);
/// Mix Station now unlocked by default, unlock all tracks immediately
if (m_NameHash == ATSTRINGHASH("RADIO_22_DLC_BATTLE_MIX1_RADIO", 0xF8BEAA16))
{
UnlockTrackList("BATTLE_MIX1_RADIO");
UnlockTrackList("BATTLE_MIX2_RADIO");
UnlockTrackList("BATTLE_MIX3_RADIO");
UnlockTrackList("BATTLE_MIX4_RADIO");
}
#if __BANK
u32 totalNumTracks = 0;
for(s32 i = 0; i < NUM_RADIO_TRACK_CATS; i++)
{
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(i);
if(trackLists)
{
const u32 numTracks = trackLists->ComputeNumTracks();
audDisplayf("%s %s: %u tracks", GetName(), TrackCats_ToString((TrackCats)i), numTracks);
totalNumTracks += numTracks;
}
}
audDebugf1("%s has %u total tracks", GetName(), totalNumTracks);
#endif
m_MusicRunningCount = 0;
/* Mix Station now unlocked by default, no longer required
// Lock DJ music station by default, relies on script mechanism to unlock
if (m_NameHash == ATSTRINGHASH("RADIO_22_DLC_BATTLE_MIX1_RADIO", 0xF8BEAA16))
{
SetLocked(true);
}
*/
}
void audRadioStation::RandomizeCategoryStrides(bool randomizeStartTrack, bool maintainDirection)
{
// Assign a random start index and stride for all required track categories
for (u32 category = 0; category < NUM_RADIO_TRACK_CATS; category++)
{
RandomizeCategoryStride((TrackCats)category, randomizeStartTrack, maintainDirection);
}
}
void audRadioStation::RandomizeCategoryStride(TrackCats category, bool randomizeStartTrack, bool maintainDirection)
{
if (IsRandomizedStrideCategory(category))
{
u32 numTracksAvailable = ComputeNumTracksAvailable(category);
if(numTracksAvailable > 0)
{
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(category);
// If this station has old and new music we want to generate separate stride/index values for both the
// track lists so that we can step through them independently (as if we just treat it as one big list
// we're going to hear all the old then all the new music if we have a stride value if +1/-1
if(trackLists && trackLists->GetUnlockedListCount() == 2)
{
audDisplayf("Station %s is in old/new stride mode for category %s", GetName(), TrackCats_ToString(category));
u32 unlockedListIndex = 0;
for(u32 i = 0; i < trackLists->GetListCount(); i++)
{
if(trackLists->IsTrackListUnlocked(trackLists->GetList(i)))
{
if(unlockedListIndex == 0)
{
RandomizeCategoryStride(category, trackLists->GetList(i)->numTracks, randomizeStartTrack, maintainDirection);
}
else
{
TrackCats takeoverCategory= TRACKCATS_MAX;
if(category == RADIO_TRACK_CAT_MUSIC) { takeoverCategory = RADIO_TRACK_CAT_TAKEOVER_MUSIC; }
if(category == RADIO_TRACK_CAT_IDENTS) { takeoverCategory = RADIO_TRACK_CAT_TAKEOVER_IDENTS; }
if(category == RADIO_TRACK_CAT_DJSOLO) { takeoverCategory = RADIO_TRACK_CAT_TAKEOVER_DJSOLO; }
RandomizeCategoryStride(takeoverCategory, trackLists->GetList(i)->numTracks, randomizeStartTrack, maintainDirection);
}
unlockedListIndex++;
}
}
}
else
{
RandomizeCategoryStride(category, numTracksAvailable, randomizeStartTrack, maintainDirection);
}
}
}
}
void audRadioStation::RandomizeCategoryStride(TrackCats category, u32 numTracksAvailable, bool randomizeStartTrack, bool maintainDirection)
{
if (numTracksAvailable > 0)
{
// Pick a random stride per-category (current either just steps forwards or backwards through the tracks)
// Nb. Don't go +/- 8! This gets packed into a 4 bit value for network syncing
if(!maintainDirection)
{
m_CategoryStride[category] = GetRandomNumberInRange(0.0f, 1.0f) >= 0.5f ? -1 : 1;
}
// Normalize back to -1 or 1 depending on previously picked direction
else
{
m_CategoryStride[category] = m_CategoryStride[category] > 0 ? 1 : -1;
}
// If we've got an odd number of tracks, we can step through two at a time for extra variety without repeating
if (numTracksAvailable % 2 == 1 && GetRandomNumberInRange(0.0f, 1.0f) >= 0.5f)
{
m_CategoryStride[category] *= 2;
}
if (audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(category)))
{
if(randomizeStartTrack || (*history)[0] == -1)
{
// Pick a random start track per-category
(*history)[0] = GetRandomNumberInRange(0, numTracksAvailable - 1);
history->SetWriteIndex(1);
}
}
}
}
// Any logic that needs to be applied after DLC tracks have been merged should go here
void audRadioStation::DLCInit()
{
if (!m_DLCInitialized)
{
// USB station track lists are all locked by default
if (IsUSBMixStation())
{
#if __BANK
g_NumUSBStationTrackLists = 0;
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
for (u32 i = 0; i < trackListCollection->GetListCount(); i++)
{
g_USBTrackListNames[g_NumUSBStationTrackLists++] = audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackListCollection->GetList(i)->NameTableOffset);
}
}
if(g_USBStationTrackListCombo)
{
g_USBStationTrackListCombo->UpdateCombo("Track List", &g_RequestedUSBTrackListComboIndex, g_NumUSBStationTrackLists, g_USBTrackListNames.GetElements());
}
#endif
ForceMusicTrackList(ATSTRINGHASH("TUNER_AP_SILENCE_MUSIC", 0x68865F9), 0u);
}
// USB tracks are all locked by default so no point randomizing
if (m_PlaySequentialMusicTracks && !IsUSBMixStation())
{
RandomizeSequentialStationPlayPosition();
}
m_DLCInitialized = true;
}
}
void audRadioStation::RandomizeSequentialStationPlayPosition()
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC));
const s32 numTracks = ComputeNumTracksAvailable(RADIO_TRACK_CAT_MUSIC);
bool hasValidTracks = history && numTracks >= 1;
audAssertf(hasValidTracks || IsUSBMixStation(), "Attempting to play sequential music tracks but track list is invalid or the list has less than one track");
if (hasValidTracks)
{
u32 firstTrackIndex = 0;
u32 lastTrackIndex = 0;
s32 tuneablePrioritizedTrackListIndex = IsNetworkModeHistoryActive() ? GetTuneablePrioritizedMusicTrackListIndex(firstTrackIndex, lastTrackIndex) : -1;
m_FirstTrackStartOffset = GetRandomNumberInRange(0.1f, 0.8f);
if (tuneablePrioritizedTrackListIndex >= 0)
{
(*history)[0] = (u32)GetRandomNumberInRange((s32)firstTrackIndex, (s32)lastTrackIndex);
audDisplayf("Radio station %s has randomised to %.02f%% through track %u/%u (prioritized track list %d)", GetName(), m_FirstTrackStartOffset * 100.f, (*history)[0], numTracks, tuneablePrioritizedTrackListIndex);
}
else
{
(*history)[0] = (u32)GetRandomNumberInRange(0, numTracks - 1);
audDisplayf("Radio station %s has randomised to %.02f%% through track %u/%u", GetName(), m_FirstTrackStartOffset * 100.f, (*history)[0], numTracks);
}
history->SetWriteIndex(1);
}
}
s32 audRadioStation::GetTuneablePrioritizedMusicTrackListIndex(u32& firstTrackIndex, u32& lastTrackIndex)
{
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC);
f32* trackListProbabilities = Alloca(f32, trackLists->GetListCount());
bool foundCustomProbabilityValue = false;
f32 probabilitySum = 0.f;
for (u32 trackListIndex = 0; trackListIndex < trackLists->GetListCount(); trackListIndex++)
{
f32 trackListProbability = 0.0f;
if (!trackLists->IsListLocked(trackListIndex))
{
trackListProbability = Max(0.0f, trackLists->GetListFirstTrackProbability(trackListIndex));
foundCustomProbabilityValue |= trackListProbability != 1.f;
}
trackListProbabilities[trackListIndex] = trackListProbability;
probabilitySum += trackListProbability;
}
if (foundCustomProbabilityValue)
{
audDisplayf("Found first music track probability modifier on station %s (%u tracks total)", GetName(), ComputeNumTracksAvailable(RADIO_TRACK_CAT_MUSIC));
f32 randomTrackIndex = audEngineUtil::GetRandomNumberInRange(0.f, probabilitySum);
#if __BANK
for (u32 trackListIndex = 0; trackListIndex < trackLists->GetListCount(); trackListIndex++)
{
const bool trackListLocked = trackLists->IsListLocked(trackListIndex);
audDisplayf("Track List %d (%s) probability: %.02f, %u tracks %s",
trackListIndex,
audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackLists->GetList(trackListIndex)->NameTableOffset),
trackLists->GetListFirstTrackProbability(trackListIndex),
trackListLocked ? 0 : trackLists->GetList(trackListIndex)->numTracks,
trackListLocked ? "(locked)" : "");
}
audDisplayf("Random track list index: %.02f", randomTrackIndex);
#endif
for (u32 trackListIndex = 0; trackListIndex < trackLists->GetListCount(); trackListIndex++)
{
const RadioStationTrackList* trackList = trackLists->GetList(trackListIndex);
const bool trackListLocked = trackLists->IsListLocked(trackListIndex);
const u32 numTracks = trackListLocked ? 0 : trackList->numTracks;
if (trackListProbabilities[trackListIndex] > 0.f)
{
randomTrackIndex -= trackListProbabilities[trackListIndex];
if (randomTrackIndex <= 0.f)
{
lastTrackIndex = firstTrackIndex + numTracks - 1;
return trackListIndex;
}
}
firstTrackIndex += numTracks;
}
}
return -1;
}
void audRadioStation::Shutdown(void)
{
for(u32 trackIndex = 0; trackIndex < 2; trackIndex++)
{
m_Tracks[trackIndex].Shutdown();
}
m_IsFirstTrack = true;
m_LastTimeDJSpeechPlaying = 0u;
}
void audRadioStation::LogTrack(const u32 BANK_ONLY(category), const u32 BANK_ONLY(context), const u32 BANK_ONLY(index), const u32 BANK_ONLY(soundRef))
{
#if __BANK
char buf[256];
formatf(buf, "%s,%u,%s,%u,%u,%s\r\n",
GetName(),
g_AudioEngine.GetSoundManager().GetTimeInMilliseconds(2),
TrackCats_ToString((TrackCats)category),
index,
context,
g_AudioEngine.GetSoundManager().GetFactory().GetMetadataManager().GetObjectName(soundRef));
if(g_LogRadio && m_ShouldStreamPhysically)
{
FileHandle fi = CFileMgr::OpenFileForAppending("common:/RadioLog.csv");
if(fi != INVALID_FILE_HANDLE)
{
CFileMgr::Write(fi, buf, istrlen(buf));
CFileMgr::CloseFile(fi);
}
}
if(IsNetworkModeHistoryActive())
{
audDebugf1("NETRADIO,%s", buf);
}
#endif
}
u32 audRadioStation::PackCategoryForNetwork(const u32 category)
{
if(category >= NUM_RADIO_TRACK_CATS)
{
return 0;
}
return category+1;
}
u32 audRadioStation::UnpackCategoryFromNetwork(const u32 category)
{
if(category == 0)
{
return 0xFFFF;
}
return category - 1;
}
bool audRadioStation::ShouldAddCategoryToNetworkHistory(const u32 category)
{
return (category == RADIO_TRACK_CAT_MUSIC || category == RADIO_TRACK_CAT_DJSOLO || category == RADIO_TRACK_CAT_ADVERTS
|| category == RADIO_TRACK_CAT_TAKEOVER_MUSIC || category == RADIO_TRACK_CAT_TAKEOVER_DJSOLO);
}
void audRadioStation::Update(u32 timeInMs)
{
// Need to defer doing this until the sound data has been loaded for a given pack
if (m_IsMixStation && !IsUSBMixStation() && !m_HasConstructedMixStationBeatMap)
{
ConstructMixStationBeatMarkerList();
m_HasConstructedMixStationBeatMap = true;
}
bool disableNetworkModeForStation = m_UseRandomizedStrideSelection;
WIN32PC_ONLY(disableNetworkModeForStation |= m_IsUserStation);
//Reset the TrackChanged flag
m_bHasTrackChanged = false;
if(!m_ShouldStreamPhysically)
{
//We can always select full radio content when this station is streaming virtually.
m_ShouldPlayFullRadio = true;
}
if(!m_Tracks[m_ActiveTrackIndex].IsInitialised())
{
if(m_IsPlayingOverlappedTrack)
{
//Our active track has finished, but we're already overlapping playback of the next track.
//Make the overlapped track our active track.
m_bHasTrackChanged = true;
m_ActiveTrackIndex = (m_ActiveTrackIndex + 1) % 2;
m_IsPlayingOverlappedTrack = false;
m_IsRequestingOverlappedTrackPrepare = false;
if(!disableNetworkModeForStation && (IsNetworkModeHistoryActive() || PARAM_disableradiohistory.Get()))
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC));
if(history)
{
if(m_Tracks[m_ActiveTrackIndex].GetSoundRef() != ATSTRINGHASH("dlc_ch_finale_radio_djsolo", 0x14F5707))
{
if(ShouldAddCategoryToNetworkHistory(m_Tracks[m_ActiveTrackIndex].GetCategory()))
{
history->AddToHistory(m_Tracks[m_ActiveTrackIndex].GetRefForHistory());
}
}
}
}
//Give the management code a frame to catch the track transition before attempting to select
//and prepare the next track.
return;
}
else
{
u32 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
//We need to prepare a track to get us started.
if(!m_Tracks[nextTrackIndex].IsInitialised())
{
InitialiseNextTrack(timeInMs);
}
audPrepareState state = PrepareNextTrack();
switch(state)
{
case AUD_PREPARED:
m_bHasTrackChanged = true;
m_ActiveTrackIndex = (m_ActiveTrackIndex + 1) % 2;
m_Tracks[m_ActiveTrackIndex].Play();
if(!disableNetworkModeForStation && (IsNetworkModeHistoryActive() || PARAM_disableradiohistory.Get()))
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC));
if(history)
{
if(ShouldAddCategoryToNetworkHistory(m_Tracks[m_ActiveTrackIndex].GetCategory()))
{
history->AddToHistory(m_Tracks[m_ActiveTrackIndex].GetRefForHistory());
}
}
}
#if RSG_PC
m_LastPlayedTrackIndex = m_CurrentlyPlayingTrackIndex;
m_CurrentlyPlayingTrackIndex = m_Tracks[m_ActiveTrackIndex].GetTrackIndex();
m_BadTrackCount = 0; // Reset bad track count. We got a good one.
#endif
break;
case AUD_PREPARE_FAILED:
{
OUTPUT_ONLY(char* nextTrackName = (char *)m_Tracks[nextTrackIndex].GetBankName();)
naErrorf("Failed to prepare a track (%s) for a radio station (%s)", nextTrackName ? nextTrackName : "Unknown", GetName());
//Kill the next track and try to prepare another next frame.
m_Tracks[nextTrackIndex].Shutdown();
#if RSG_PC
if(m_IsUserStation)
{
m_BadTrackCount++;
Warningf("BadTrackCount: %u", m_BadTrackCount);
}
#endif
}
//Intentional fall-through.
case AUD_PREPARING:
default:
return;
}
}
}
else if(IsNetworkModeHistoryActive() && m_Tracks[m_ActiveTrackIndex].IsInitialised() && m_Tracks[m_ActiveTrackIndex].IsDormant())
{
// B*1504812
audWarningf("%s - Should be playing but track state is dormant, requesting play", GetName());
m_Tracks[m_ActiveTrackIndex].PlayWhenReady();
}
m_Tracks[m_ActiveTrackIndex].Update(timeInMs, IsFrozen());
//
//We're playing an active track, so prepare the next track.
//
#if RSG_PC // user music
if(m_IsUserStation)
{
USERRADIO_PLAYMODE playMode = sm_UserRadioTrackManager.GetRadioMode();
if(playMode == USERRADIO_PLAY_MODE_SEQUENTIAL && (sm_ForceUserPrevTrack || sm_ForceUserNextTrack))
{
const s32 numTracks = sm_UserRadioTrackManager.GetNumTracks();
if(numTracks > 0)
{
if(sm_ForceUserPrevTrack)
{
sm_UserRadioTrackManager.SetNextTrack((m_Tracks[m_ActiveTrackIndex].GetTrackIndex() + numTracks - 1) % numTracks);
}
else
{
sm_UserRadioTrackManager.SetNextTrack((m_Tracks[m_ActiveTrackIndex].GetTrackIndex() + 1) % numTracks);
}
}
}
m_ShouldPlayFullRadio = (sm_IsInRecoveryMode || playMode == USERRADIO_PLAY_MODE_RADIO);
// ditch current track
if(sm_ForceUserNextTrack || sm_ForceUserPrevTrack)
{
m_Tracks[m_ActiveTrackIndex].Shutdown();
}
// if we're going backwards, ditch the next track too
if(sm_ForceUserPrevTrack || (playMode == USERRADIO_PLAY_MODE_SEQUENTIAL && sm_ForceUserNextTrack))
{
u32 nextTrackIndex = (m_ActiveTrackIndex+1)%2;
m_Tracks[nextTrackIndex].Shutdown();
}
if(sm_ForceUserNextTrack || sm_ForceUserPrevTrack)
{
sm_ForceUserNextTrack = false;
sm_ForceUserPrevTrack = false;
return;
}
sm_ForceUserNextTrack = false;
sm_ForceUserPrevTrack = false;
if(m_Tracks[m_ActiveTrackIndex].IsInitialised() && m_Tracks[m_ActiveTrackIndex].IsStreamingPhysically() && m_Tracks[m_ActiveTrackIndex].GetPlayTime() > 1000 && m_Tracks[m_ActiveTrackIndex].GetCategory() == RADIO_TRACK_CAT_MUSIC)
{
// we've succesfully played a user track; recovery mode is no longer required
sm_IsInRecoveryMode = false;
}
}
#endif
u8 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
if(m_IsPlayingOverlappedTrack)
{
//Sanity-check that we are playing a track of the correct category.
if(ShouldOnlyPlayMusic() && m_Tracks[nextTrackIndex].GetCategory() != RADIO_TRACK_CAT_MUSIC)
{
//We should only choose music.
Assert(!IsFrozen());
//Stop playing this track and allow a music track to be selected next frame.
audDisplayf("Station %s should only play music, but overlapped track (%u) is not music - shutting down", GetName(), m_Tracks[nextTrackIndex].GetSoundRef());
m_Tracks[nextTrackIndex].Shutdown();
m_IsPlayingOverlappedTrack = false;
m_IsRequestingOverlappedTrackPrepare = false;
return;
}
m_Tracks[nextTrackIndex].Update(timeInMs, IsFrozen());
}
else
{
//Sanity-check that we are playing a track of the correct category.
if(ShouldOnlyPlayMusic() && m_Tracks[m_ActiveTrackIndex].GetCategory() != RADIO_TRACK_CAT_MUSIC)
{
//We should only choose music.
Assert(!IsFrozen());
//Stop playing this track and allow a music track to be selected next frame.
audDisplayf("Station %s should only play music, but active track (%u) is not music - shutting down", GetName(), m_Tracks[m_ActiveTrackIndex].GetSoundRef());
m_Tracks[m_ActiveTrackIndex].Shutdown();
return;
}
if(!m_Tracks[nextTrackIndex].IsInitialised())
{
InitialiseNextTrack(timeInMs);
}
//Prepare the next track.
audRadioTrack &activeTrack = m_Tracks[m_ActiveTrackIndex];
s32 playTime = activeTrack.GetPlayTime();
s32 duration = activeTrack.GetDuration();
// Wait until the active track is in a playing state before attempting to prepare the next track for the overlap, otherwise
// retuning to a track within the overlap window can cause problems.
if(activeTrack.IsPlayingPhysicallyOrVirtually() && playTime >= (duration - g_RadioTrackPrepareTimeMs) && !sm_DisableOverlap)
{
if(m_IsRequestingOverlappedTrackPrepare)
{
audPrepareState state = PrepareNextTrack();
if(state == AUD_PREPARED)
{
s32 compensatedPlayTime = playTime;
if(!g_DisablePlayTimeCompensation && !m_IsMixStation && !m_PlaySequentialMusicTracks && !disableNetworkModeForStation && (IsNetworkModeHistoryActive() || PARAM_disableradiohistory.Get()))
{
// compensate for drift by sliding the overlap
compensatedPlayTime = Clamp(static_cast<s32>(audDriver::GetMixer()->GetMixerTimeMs() - activeTrack.GetTimePlayed()),
playTime - 500,
playTime + 500);
// TODO: accumulate error when >500ms
}
m_IsPlayingMixTransition = m_Tracks[nextTrackIndex].IsFlyloPart1() != m_Tracks[m_ActiveTrackIndex].IsFlyloPart1();
u32 overlapTimeMs = WIN32PC_ONLY(m_IsUserStation ? g_UserRadioTrackOverlapTimeMs : )g_RadioTrackOverlapTimeMs;
u32 overlapTimePlayMs = overlapTimeMs;
if (g_UsePreciseTrackCrossfades && m_PlaySequentialMusicTracks && m_Tracks[nextTrackIndex].GetStreamingSound())
{
overlapTimePlayMs += g_RadioTrackOverlapPlayTimeMs;
}
if(compensatedPlayTime >= (duration - overlapTimePlayMs))
{
if (g_UsePreciseTrackCrossfades && m_PlaySequentialMusicTracks && m_Tracks[nextTrackIndex].GetStreamingSound())
{
u32 trackTimeRemaining = duration - playTime;
u32 trackTimeCalculationFrame = activeTrack.GetPlayTimeCalculationMixerFrame();
f32 trackStartMixerFrames = trackTimeCalculationFrame + ((trackTimeRemaining - overlapTimeMs) / (g_SecondsPerMixBuffer * 1000));
#if RSG_ORBIS
trackStartMixerFrames += 1168 / (f32)kMixBufNumSamples;
#endif
u32 startOffsetFrames = (u32)floor(trackStartMixerFrames);
u16 subFrameOffset = (u16)((trackStartMixerFrames - startOffsetFrames) * kMixBufNumSamples);
m_Tracks[nextTrackIndex].GetStreamingSound()->SetRequestedMixerPlayTimeAbsolute(startOffsetFrames, subFrameOffset);
}
m_Tracks[nextTrackIndex].Play();
m_IsPlayingOverlappedTrack = true;
#if RSG_PC
m_LastPlayedTrackIndex = m_CurrentlyPlayingTrackIndex;
m_CurrentlyPlayingTrackIndex = m_Tracks[nextTrackIndex].GetTrackIndex();
m_BadTrackCount = 0;
#endif
}
}
#if RSG_PC // this block came from GTA IV user music, is it still needed? SORR
else if(state == AUD_PREPARE_FAILED)
{
//Kill the next track and try to prepare another next frame.
m_Tracks[nextTrackIndex].Shutdown();
m_BadTrackCount++;
Warningf("BadTrackCount: %u", m_BadTrackCount);
}
#endif
}
m_IsRequestingOverlappedTrackPrepare = true;
}
else
{
m_IsRequestingOverlappedTrackPrepare = false;
}
}
if(m_ShouldStreamPhysically)
{
if(audRadioSlot::FindSlotByStation(this) == NULL)
{
naWarningf("Station %s playing physically with no valid slot; forcing virtual streaming", GetName());
SetPhysicalStreamingState(false);
}
}
#if __BANK
if(m_bHasTrackChanged)
{
audDisplayf("Track changed on radio station %s - new track %s (%s track %d) - offset %.02f, network time %u", GetName(), SOUNDFACTORY.GetMetadataManager().GetObjectName(m_Tracks[m_ActiveTrackIndex].GetSoundRef()), m_Tracks[m_ActiveTrackIndex].GetCategory() >= NUM_RADIO_TRACK_CATS ? "Invalid" : TrackCats_ToString((TrackCats)m_Tracks[m_ActiveTrackIndex].GetCategory()), m_Tracks[m_ActiveTrackIndex].GetTrackIndex(), m_Tracks[m_ActiveTrackIndex].GetPlayFraction(), NetworkInterface::GetSyncedTimeInMilliseconds());
}
#endif
}
bool audRadioStation::IsStreamingPhysically() const
{
for(u32 trackIndex = 0; trackIndex < 2; trackIndex++)
{
if(m_Tracks[trackIndex].IsInitialised())
{
if(m_Tracks[trackIndex].IsStreamingPhysically())
{
return true;
}
}
}
return false;
}
bool audRadioStation::IsStreamingVirtually() const
{
bool isStreamingVirtually = false;
if(m_Tracks[m_ActiveTrackIndex].IsInitialised())
{
isStreamingVirtually = m_Tracks[m_ActiveTrackIndex].IsStreamingVirtually();
}
//Also check the next track if need be.
u8 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
if(isStreamingVirtually && m_Tracks[nextTrackIndex].IsInitialised())
{
isStreamingVirtually = m_Tracks[nextTrackIndex].IsStreamingVirtually();
}
return isStreamingVirtually;
}
void audRadioStation::SetStreamSlots(audStreamSlot **streamSlots)
{
m_WaveSlots[0] = NULL;
m_WaveSlots[1] = NULL;
if(streamSlots)
{
if(streamSlots[0])
{
m_WaveSlots[0] = streamSlots[0]->GetWaveSlot();
}
if(streamSlots[1])
{
m_WaveSlots[1] = streamSlots[1]->GetWaveSlot();
}
}
}
void audRadioStation::SetPhysicalStreamingState(bool shouldStreamPhysically, audStreamSlot **streamSlots, u8 soundBucketId)
{
m_ShouldStreamPhysically = shouldStreamPhysically;
m_SoundBucketId = soundBucketId;
m_WaveSlots[0] = NULL;
m_WaveSlots[1] = NULL;
if(streamSlots)
{
if(streamSlots[0])
{
m_WaveSlots[0] = streamSlots[0]->GetWaveSlot();
}
if(streamSlots[1])
{
m_WaveSlots[1] = streamSlots[1]->GetWaveSlot();
}
}
bool disableNetworkModeForStation = false;
WIN32PC_ONLY(disableNetworkModeForStation = m_IsUserStation);
audRadioTrack &activeTrack = m_Tracks[m_ActiveTrackIndex];
if(activeTrack.IsInitialised())
{
bool shouldSkipEnd = false;
s32 playTime = activeTrack.GetPlayTime();
s32 duration = (s32)activeTrack.GetDuration();
if(!disableNetworkModeForStation && (IsNetworkModeHistoryActive() || PARAM_disableradiohistory.Get()))
{
shouldSkipEnd = playTime >= (duration - 2000U);
}
else if(playTime >= (duration - g_RadioTrackSkipEndTimeMs))
{
shouldSkipEnd = true;
}
if(!IsFrozen() && m_ShouldStreamPhysically && (shouldSkipEnd || m_IsPlayingOverlappedTrack))
{
//We are about to physically stream the end of a track or an overlap,
//so stop the finishing track to prevent startup problems.
activeTrack.Shutdown();
}
else
{
activeTrack.SetPhysicalStreamingState(shouldStreamPhysically, m_WaveSlots[m_ActiveTrackIndex], soundBucketId);
}
}
const u32 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
audRadioTrack &nextTrack = m_Tracks[nextTrackIndex];
if(nextTrack.IsInitialised())
{
nextTrack.SetPhysicalStreamingState(shouldStreamPhysically, m_WaveSlots[nextTrackIndex], soundBucketId);
}
}
void audRadioStation::AddActiveDjSpeechToHistory(u32 timeInMs)
{
//Make sure we didn't already add this track.
u8 previousHistoryWriteIndex = (m_DjSpeechHistoryWriteIndex + g_MaxDjSpeechInHistory - 1) %
g_MaxDjSpeechInHistory;
if(m_DjSpeechHistory[previousHistoryWriteIndex] != sm_ActiveDjSpeechId)
{
// Speech category could potentially be invalid if we've chosen to trigger some dummy DJ speech
if(sm_ActiveDjSpeechCategory < NUM_RADIO_DJ_SPEECH_CATS)
{
RADIODEBUG3("DjSpeech %s:%u", g_RadioDjSpeechCategoryNames[sm_ActiveDjSpeechCategory], sm_ActiveDjSpeechId);
m_DjSpeechHistory[m_DjSpeechHistoryWriteIndex] = sm_ActiveDjSpeechId;
m_DjSpeechHistoryWriteIndex = (m_DjSpeechHistoryWriteIndex + 1) % g_MaxDjSpeechInHistory;
const u32 repeatTime = (m_NameHash == ATSTRINGHASH("RADIO_34_DLC_HEI4_KULT", 0xE3442163)) ? g_RadioDjSpeechMinTimeBetweenRepeatedCategoriesKult[sm_ActiveDjSpeechCategory] : g_RadioDjSpeechMinTimeBetweenRepeatedCategories[sm_ActiveDjSpeechCategory];
//Log the next time that this category can be selected.
m_DjSpeechCategoryNextValidSelectionTime[sm_ActiveDjSpeechCategory] = timeInMs + repeatTime;
}
}
}
u32 audRadioStation::CreateCompatibleDjSpeech(u32 timeInMs, u8 preferredCategory, s32 windowLengthMs, const s32 windowStartTime) const
{
naCErrorf(preferredCategory < NUM_RADIO_DJ_SPEECH_CATS, "Invalid preferred category passed into CreateCompatibleDjSpeech");
audDebugf1("DJSPEECH CreateCompatibleDjSpeech: %s, %d, %d", g_RadioDjSpeechCategoryNames[preferredCategory], windowLengthMs, windowStartTime);
u32 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
u32 nextTrackCategory = m_Tracks[nextTrackIndex].GetCategory();
const bool inTakeoverState = m_Tracks[m_ActiveTrackIndex].GetCategory() == RADIO_TRACK_CAT_TAKEOVER_MUSIC;
// no TIME DJ speech for iFruit in takeover mode or MOTOMAMI
const bool disableTimeCategory = inTakeoverState || m_NameHash == ATSTRINGHASH("RADIO_37_MOTOMAMI", 0x97074FCC);
if(preferredCategory == RADIO_DJ_SPEECH_CAT_INTRO)
{
//Check if we should try and play time of day banter, ensuring that we don't hear this too often.
// NOTE: using global random stream for dj speech
if(!disableTimeCategory && audEngineUtil::ResolveProbability(g_RadioDjTimeProbability) &&
(timeInMs >= m_DjSpeechCategoryNextValidSelectionTime[RADIO_DJ_SPEECH_CAT_TIME]))
{
preferredCategory = RADIO_DJ_SPEECH_CAT_TIME;
}
}
else if(preferredCategory == RADIO_DJ_SPEECH_CAT_OUTRO) //NOTE: We don't currently have track-specific outros.
{
//Override outro speech with to... speech if we are playing a relevant track category next.
if((nextTrackCategory == RADIO_TRACK_CAT_ADVERTS) || (nextTrackCategory == RADIO_TRACK_CAT_NEWS) ||
(nextTrackCategory == RADIO_TRACK_CAT_WEATHER))
{
preferredCategory = RADIO_DJ_SPEECH_CAT_TO;
}
//Check if we should try and play time of day banter, ensuring that we don't hear this too often.
// NOTE: using global random stream for dj speech
else if(!inTakeoverState && audEngineUtil::ResolveProbability(g_RadioDjTimeProbability) &&
(timeInMs >= m_DjSpeechCategoryNextValidSelectionTime[RADIO_DJ_SPEECH_CAT_TIME]))
{
preferredCategory = RADIO_DJ_SPEECH_CAT_TIME;
}
else
{
preferredCategory = RADIO_DJ_SPEECH_CAT_GENERAL;
}
}
const char *categoryName = g_RadioDjSpeechCategoryNames[preferredCategory];
// Override GENERAL with TAKEOVER_GENERAL if we're in a takeover state, or DD_GENERAL for WCC launch event
if(preferredCategory == RADIO_DJ_SPEECH_CAT_GENERAL)
{
if(inTakeoverState)
{
categoryName = "TAKEOVER_GENERAL";
}
else if((m_NameHash == ATSTRINGHASH("RADIO_09_HIPHOP_OLD", 0x572C04) && IsTrackListUnlocked(ATSTRINGHASH("RADIO_09_HIPHOP_OLD_DD_MUSIC_LAUNCH", 0x2018C9AF))) ||
(m_NameHash == ATSTRINGHASH("RADIO_03_HIPHOP_NEW", 0xFA17DE37) && IsTrackListUnlocked(ATSTRINGHASH("RADIO_03_HIPHOP_NEW_DD_MUSIC_LAUNCH", 0x773C7204))))
{
categoryName = "DD_GENERAL";
}
else if((m_NameHash == ATSTRINGHASH("RADIO_09_HIPHOP_OLD", 0x572C04) && IsTrackListUnlocked(ATSTRINGHASH("RADIO_09_HIPHOP_OLD_DD_MUSIC_POST_LAUNCH", 0x9ACEDFBE))) ||
(m_NameHash == ATSTRINGHASH("RADIO_03_HIPHOP_NEW", 0xFA17DE37) && IsTrackListUnlocked(ATSTRINGHASH("RADIO_03_HIPHOP_NEW_DD_MUSIC_POST_LAUNCH", 0x3ED8B9F2))))
{
categoryName = "PL_GENERAL";
}
}
formatf(g_RadioDjVoiceName, "DJ_%s_%s", GetName(), categoryName);
//Determine the correct context name.
s32 gameHours = CClock::GetHour();
g_RadioDjContextName[0] = '\0';
char *bankName;
switch(preferredCategory)
{
case RADIO_DJ_SPEECH_CAT_INTRO:
case RADIO_DJ_SPEECH_CAT_OUTRO: //Intentional fall-through.
//The context name is the name of the streaming wave bank for specific intros and outros.
// If the current track has multiple text Ids then we fall back to using the text id, rather than bank name (ie mix stations)
if(m_Tracks[m_ActiveTrackIndex].GetNumTextIds() > 1)
{
char trackName[64];
formatf(trackName, "TRACK%u", m_Tracks[m_ActiveTrackIndex].GetTextId(static_cast<u32>(windowStartTime)));
strncpy(g_RadioDjContextName, trackName, g_MaxRadioNameLength);
}
else
{
bankName = (char *)m_Tracks[m_ActiveTrackIndex].GetBankName();
if(bankName)
{
const char *trackName = (const char *)strrchr(bankName, '\\');
if(trackName)
{
strncpy(g_RadioDjContextName, trackName + 1, g_MaxRadioNameLength);
}
}
}
break;
case RADIO_DJ_SPEECH_CAT_TIME:
if((gameHours >= (s32)g_RadioDjTimeMorningStart) &&
(gameHours < (s32)g_RadioDjTimeMorningEnd))
{
strncpy(g_RadioDjContextName, g_RadioDjMorningContext, g_MaxRadioNameLength);
}
else if((gameHours >= (s32)g_RadioDjTimeAfternoonStart) &&
(gameHours < (s32)g_RadioDjTimeAfternoonEnd))
{
strncpy(g_RadioDjContextName, g_RadioDjAfternoonContext, g_MaxRadioNameLength);
}
else if((gameHours >= (s32)g_RadioDjTimeEveningStart) &&
(gameHours < (s32)g_RadioDjTimeEveningEnd))
{
strncpy(g_RadioDjContextName, g_RadioDjEveningContext, g_MaxRadioNameLength);
}
else if((gameHours >= (s32)g_RadioDjTimeNightStart) ||
(gameHours < (s32)g_RadioDjTimeNightEnd))
{
strncpy(g_RadioDjContextName, g_RadioDjNightContext, g_MaxRadioNameLength);
}
break;
case RADIO_DJ_SPEECH_CAT_TO:
switch(nextTrackCategory)
{
case RADIO_TRACK_CAT_ADVERTS:
strncpy(g_RadioDjContextName, g_RadioDjToAdvertContext, g_MaxRadioNameLength);
break;
case RADIO_TRACK_CAT_NEWS:
strncpy(g_RadioDjContextName, g_RadioDjToNewsContext, g_MaxRadioNameLength);
break;
case RADIO_TRACK_CAT_WEATHER:
strncpy(g_RadioDjContextName, g_RadioDjToWeatherContext, g_MaxRadioNameLength);
break;
default:
break;
}
break;
//case RADIO_DJ_SPEECH_CAT_GENERAL:
default:
strncpy(g_RadioDjContextName, categoryName, g_MaxRadioNameLength);
}
u32 variationNum;
u32 speechLengthMs = FindCompatibleDjSpeechVariation(g_RadioDjVoiceName, g_RadioDjContextName, -1, windowLengthMs, variationNum,
preferredCategory, false);
if(speechLengthMs > 0)
{
CreateDjSpeechSounds(variationNum);
}
else
{
switch(preferredCategory)
{
case RADIO_DJ_SPEECH_CAT_GENERAL:
//No additional fall-back categories.
break;
//case RADIO_DJ_SPEECH_CAT_INTRO:
//case RADIO_DJ_SPEECH_CAT_OUTRO:
//case RADIO_DJ_SPEECH_CAT_TIME:
//case RADIO_DJ_SPEECH_CAT_TO:
default:
if(!inTakeoverState)
{
//See if we have any general banter that is compatible as a fall-back.
// Don't want to do this if in a takeover segment as the takeover generals haven't been written to
// work as intros following a handover.
speechLengthMs = CreateCompatibleDjSpeech(timeInMs, RADIO_DJ_SPEECH_CAT_GENERAL, windowLengthMs, windowStartTime);
}
}
}
return speechLengthMs;
}
void audRadioStation::PrepareIMDrivenDjSpeech(const u32 category, const u32 variationNum)
{
// TODO: deal with playing?
if(sm_DjSpeechMonoSound)
{
sm_DjSpeechMonoSound->StopAndForget();
}
if(sm_DjSpeechPositionedSound)
{
sm_DjSpeechPositionedSound->StopAndForget();
}
formatf(g_RadioDjVoiceName, "DJ_%s_%s", GetName(), g_RadioDjSpeechCategoryNames[category]);
formatf(g_RadioDjContextName, "%s", g_RadioDjSpeechCategoryNames[category]);
sm_IMDrivenDjSpeech = true;
sm_IMDrivenDjSpeechScheduled = false;
sm_IMDjSpeechStation = this;
audDisplayf("IM Dj Speech: Preparing %s / %s variation %u", g_RadioDjVoiceName, g_RadioDjContextName, variationNum);
CreateDjSpeechSounds(variationNum);
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_PREPARING;
}
void audRadioStation::TriggerIMDrivenDjSpeech(const u32 startTimeMs)
{
sm_ActiveDjSpeechStartTimeMs = startTimeMs;
sm_IMDrivenDjSpeechScheduled = true;
}
void audRadioStation::CancelIMDrivenDjSpeech()
{
if(sm_DjSpeechMonoSound)
{
sm_DjSpeechMonoSound->StopAndForget();
}
if(sm_DjSpeechPositionedSound)
{
sm_DjSpeechPositionedSound->StopAndForget();
}
sm_DjSpeechState = RADIO_DJ_SPEECH_STATE_IDLE;
sm_IMDrivenDjSpeechScheduled = false;
sm_IMDrivenDjSpeech = false;
}
bool audRadioStation::ChooseDjSpeechVariation(const u32 category, u32 &variationNum, u32 &lengthMs, const bool ignoreHistory)
{
char voiceName[g_MaxRadioNameLength];
char contextName[g_MaxRadioNameLength];
formatf(voiceName, "DJ_%s_%s", GetName(), g_RadioDjSpeechCategoryNames[category]);
formatf(contextName, "%s", g_RadioDjSpeechCategoryNames[category]);
lengthMs = FindCompatibleDjSpeechVariation(voiceName, contextName, 5000, 20000, variationNum, (u8)category, ignoreHistory);
if(lengthMs)
{
return true;
}
else
{
return false;
}
}
void audRadioStation::CreateDjSpeechSounds(const u32 variationNum)
{
audSoundInitParams initParams;
initParams.TimerId = 2;
g_RadioAudioEntity.CreateSound_PersistentReference(g_RadioAudioEntity.GetRadioSounds().Find(ATSTRINGHASH("DjSpeechFrontend", 0x5924EF1)),
&sm_DjSpeechMonoSound, &initParams);
if(sm_DjSpeechMonoSound)
{
//sm_DjSpeechMonoSound->SetPredelay(g_RadioDjDuckTimeMs / 2);
if(((audSpeechSound *)sm_DjSpeechMonoSound)->InitSpeech(g_RadioDjVoiceName, g_RadioDjContextName, variationNum))
{
//Initiate the preparation of this speech.
sm_DjSpeechMonoSound->Prepare(g_RadioDjSpeechWaveSlot, true);
g_RadioAudioEntity.CreateSound_PersistentReference(g_RadioAudioEntity.GetRadioSounds().Find(ATSTRINGHASH("DjSpeechPositioned", 0xD5115C83)),
&sm_DjSpeechPositionedSound, &initParams);
if(sm_DjSpeechPositionedSound)
{
//sm_DjSpeechPositionedSound->SetPredelay(g_RadioDjDuckTimeMs / 2);
if(!((audSpeechSound *)sm_DjSpeechPositionedSound)->InitSpeech(g_RadioDjVoiceName, g_RadioDjContextName, variationNum))
{
sm_DjSpeechPositionedSound->StopAndForget();
sm_DjSpeechMonoSound->StopAndForget();
}
}
else
{
sm_DjSpeechMonoSound->StopAndForget();
}
}
else
{
sm_DjSpeechMonoSound->StopAndForget();
}
}
}
u32 audRadioStation::FindCompatibleDjSpeechVariation(const char *voiceName, const char *contextName, s32 minLengthMs, s32 maxLengthMs, u32 &variationNum,
u8 djSpeechCategory, const bool ignoreHistory) const
{
u32 selectedLengthMs = 0;
audDebugf1("DJSPEECH: FindCompatibleDjSpeechVariation voice name: %s, contextName %s, minLength %d, maxLength %d", voiceName, contextName, minLengthMs, maxLengthMs);
u32 numVariations;
u8 variationData[g_RadioDjMaxVariations];
u32 variationUids[g_RadioDjMaxVariations];
audSpeechSound::FindVariationInfoForVoiceAndContext(voiceName, contextName, numVariations, variationData, variationUids);
if(numVariations > 0)
{
//See if any of the variations fit within our window.
s32 numCompatibleVariations = 0;
u32 compatibleVariationIndices[g_RadioDjMaxVariations];
for(u32 i=0; i<numVariations; i++)
{
u32 speechLengthMs = (u32)(variationData[i]) * 100;
if((s32)speechLengthMs < maxLengthMs && minLengthMs <= (s32)speechLengthMs)
{
//Check if this DJ speech is in the history.
// - Make sure we don't look over too much history???
u8 historyLength = g_MaxDjSpeechInHistory;
bool isSpeechInHistory = false;
if(!ignoreHistory)
{
for(u8 historyOffset=0; historyOffset<historyLength; historyOffset++)
{
u8 historyIndex = (m_DjSpeechHistoryWriteIndex + g_MaxDjSpeechInHistory - 1 - historyOffset) %
g_MaxDjSpeechInHistory;
if(m_DjSpeechHistory[historyIndex] == variationUids[i])
{
isSpeechInHistory = true;
break;
}
}
}
if(!isSpeechInHistory)
{
// url:bugstar:7459269 - MOTOMAMI Los Santos - incorrect DJ line[Arca - KLK]
// Prevent variation _01 from being selected
if (i == 0 && m_NameHash == ATSTRINGHASH("RADIO_37_MOTOMAMI", 0x97074FCC) && djSpeechCategory == RADIO_DJ_SPEECH_CAT_INTRO && atStringHash(contextName) == ATSTRINGHASH("KLK", 0xC2A78E3B))
{
audDebugf1("DJSPEECH: FindCompatibleDjSpeechVariation ignoring variation 1 for RADIO_DJ_SPEECH_CAT_INTRO context KLK");
continue;
}
compatibleVariationIndices[numCompatibleVariations] = i;
numCompatibleVariations++;
}
}
}
audDebugf1("DJSPEECH found %u variations, %u compatible", numVariations, numCompatibleVariations);
if(numCompatibleVariations > 0)
{
//Choose a random variation from those that are compatible.
// NOTE: currently using global random stream for DJ speech, rather than per-station stream
s32 randomVariationIndex = audEngineUtil::GetRandomNumberInRange(0, numCompatibleVariations - 1);
u32 variationIndex = compatibleVariationIndices[randomVariationIndex];
//Variations are not zero-based, so add one.
variationNum = variationIndex + 1;
selectedLengthMs = (u32)(variationData[variationIndex]) * 100;
sm_ActiveDjSpeechId = variationUids[variationIndex];
sm_ActiveDjSpeechCategory = djSpeechCategory;
audDebugf1("Chose variation %u for context %s (voice %s). Length: %u (varData: %u)", variationNum, contextName, voiceName, selectedLengthMs, variationData[variationIndex]);
}
}
return selectedLengthMs;
}
void audRadioStation::UpdateStereoEmitter(f32 volumeFactor, f32 cutoff)
{
//Calculate the volume to be applied.
f32 volumeLinear = Max<f32>(sm_VehicleRadioRiseVolumeCurve.CalculateValue(volumeFactor), g_SilenceVolumeLin);
//Apply a smoothed duck when DJ speech is active.
#if __BANK
//Recompute the linear ducking level and smoothing rate, as they might have changed via widgets.
g_RadioDjDuckingLevelLin = audDriverUtil::ComputeLinearVolumeFromDb(g_RadioDjDuckingLevelDb);
f32 increaseDecreaseRate = 1.0f / (f32)g_RadioDjDuckTimeMs;
sm_DjDuckerVolumeSmoother.SetRate(increaseDecreaseRate);
#endif // __BANK
f32 djDuckingOffsetLin = sm_DjDuckerVolumeSmoother.CalculateValue(isDjSpeaking() ? g_RadioDjDuckingLevelLin : 1.0f,
g_AudioEngine.GetSoundManager().GetTimeInMilliseconds(2));
// Store stereo emitter volume for player station
if(this == g_RadioAudioEntity.GetPlayerRadioStation())
{
f32 djVolumeDb = audDriverUtil::ComputeDbVolumeFromLinear(volumeLinear);
djVolumeDb += g_RadioAudioEntity.GetVolumeOffset();
sm_DjSpeechFrontendVolume = djVolumeDb;
sm_DjSpeechFrontendLPF = cutoff;
}
// don't apply ducking to Dj
volumeLinear /= djDuckingOffsetLin;
//Apply the volume.
for(u8 trackIndex=0; trackIndex<2; trackIndex++)
{
f32 volumeDb = audDriverUtil::ComputeDbVolumeFromLinear(volumeLinear * GetMixTransitionVolumeLinear(trackIndex));
volumeDb += g_RadioAudioEntity.GetVolumeOffset();
m_Tracks[trackIndex].UpdateStereoEmitter(volumeDb,cutoff);
}
}
f32 audRadioStation::GetMixTransitionVolumeLinear(u32 trackIndex) const
{
if (m_IsPlayingMixTransition)
{
s32 playTime = m_Tracks[trackIndex].GetPlayTime();
if (playTime < g_RadioTrackMixtransitionTimeMs)
{
return Max(0, (playTime - g_RadioTrackMixtransitionOffsetTimeMs)) / (f32)g_RadioTrackMixtransitionTimeMs;
}
else if (playTime >= m_Tracks[trackIndex].GetDuration() - g_RadioTrackMixtransitionTimeMs)
{
s32 timeRemaining = Max(0, (s32)m_Tracks[trackIndex].GetDuration() - g_RadioTrackMixtransitionOffsetTimeMs - playTime);
return timeRemaining / (f32)g_RadioTrackMixtransitionTimeMs;
}
}
return 1.f;
}
#if RSG_ASSERT
void audRadioStation::ValidateEnvironmentMetric(const audEnvironmentGameMetric *occlusionMetric) const
{
char extraInfo[128];
formatf(extraInfo, "metric: %p", occlusionMetric);
ValidateEnvironmentMetricInRange(occlusionMetric->GetReverbLarge(), "ReverbLarge", extraInfo);
ValidateEnvironmentMetricInRange(occlusionMetric->GetReverbMedium(), "ReverbMedium", extraInfo);
ValidateEnvironmentMetricInRange(occlusionMetric->GetReverbSmall(), "ReverbSmall", extraInfo);
ValidateEnvironmentMetricInRange(occlusionMetric->GetDryLevel(), "DryLevel", extraInfo);
ValidateEnvironmentMetricInRange(occlusionMetric->GetLeakageFactor(), "LeakageFactor", extraInfo);
naAssertf(FPIsFinite(occlusionMetric->GetRolloffFactor()), "Non-finite RolloffFactor, %s", extraInfo);
ValidateEnvironmentMetricInRange(occlusionMetric->GetDistanceAttenuationDamping(), "DistanceAttenuationDamping", extraInfo);
ValidateEnvironmentMetricInRange(occlusionMetric->GetOcclusionAttenuationLin(), "OcclusionAttenuationLin", extraInfo);
ValidateEnvironmentMetricInRange(occlusionMetric->GetOcclusionFilterCutoffLin(), "OcclusionFilterCutoffLin", extraInfo);
if(occlusionMetric->GetUseOcclusionPath())
{
naAssertf(FPIsFinite(occlusionMetric->GetOcclusionPathDistance()), "Non-finite occlusion path distance, %s", extraInfo);
naAssertf(FPIsFinite(MagSquared(occlusionMetric->GetOcclusionPathPosition()).Getf()), "Non-finite occlusion path position, %s [%f,%f,%f]", extraInfo, occlusionMetric->GetOcclusionPathPosition().GetX().Getf(), occlusionMetric->GetOcclusionPathPosition().GetY().Getf(), occlusionMetric->GetOcclusionPathPosition().GetZ().Getf());
}
}
void audRadioStation::ValidateEnvironmentMetricInRange(const float val, const char *valName, const char *extraInfo) const
{
naAssertf(val >= 0.f && val <= 1.f, "Invalid %s value %f. %s", valName, val, extraInfo);
}
#endif // RSG_ASSERT
void audRadioStation::UpdatePositionedEmitter(const u32 emitterIndex, const f32 emittedVolume, const f32 volumeFactor,
const Vector3 &position, const u32 LPFCutoff, const u32 HPFCutoff, const audEnvironmentGameMetric *occlusionMetric, const u32 emitterType, const f32 environmentalLoudness, bool isPlayerVehicleRadioEmitter)
{
ASSERT_ONLY(ValidateEnvironmentMetric(occlusionMetric));
//Calculate the volume to be applied.
f32 volumeLinear = Max<f32>(sm_VehicleRadioRiseVolumeCurve.CalculateValue(volumeFactor), g_SilenceVolumeLin);
//Apply a smoothed duck when DJ speech is active.
#if __BANK
//Recompute the linear ducking level and smoothing rate, as they might have changed via widgets.
g_RadioDjDuckingLevelLin = audDriverUtil::ComputeLinearVolumeFromDb(g_RadioDjDuckingLevelDb);
f32 increaseDecreaseRate = 1.0f / (f32)g_RadioDjDuckTimeMs;
sm_DjDuckerVolumeSmoother.SetRate(increaseDecreaseRate);
#endif // __BANK
if(this == g_RadioAudioEntity.GetPlayerRadioStation() && g_RadioAudioEntity.GetLastPlayerVehicle())
{
// only duck for player station emitters
f32 djDuckingOffsetLin = sm_DjDuckerVolumeSmoother.CalculateValue(isDjSpeaking() ? g_RadioDjDuckingLevelLin : 1.0f,
g_AudioEngine.GetSoundManager().GetTimeInMilliseconds(2));
volumeLinear /= djDuckingOffsetLin;
}
if(isPlayerVehicleRadioEmitter)
{
sm_PositionedPlayerVehicleRadioLPFCutoff = LPFCutoff;
sm_PositionedPlayerVehicleRadioHPFCutoff = HPFCutoff;
sm_PositionedPlayerVehicleRadioEnvironmentalLoudness = environmentalLoudness;
sm_PositionedPlayerVehicleRadioVolume = emittedVolume;
sm_PositionedPlayerVehicleRadioRollOff = occlusionMetric->GetRolloffFactor();
}
//Apply the volume, cutoff and position to the emitter.
for(u8 trackIndex=0; trackIndex<2; trackIndex++)
{
f32 volumeOffsetDb = audDriverUtil::ComputeDbVolumeFromLinear(volumeLinear * GetMixTransitionVolumeLinear(trackIndex));
volumeOffsetDb += g_RadioAudioEntity.GetVolumeOffset();
m_Tracks[trackIndex].UpdatePositionedEmitter(emitterIndex, emittedVolume, volumeOffsetDb, LPFCutoff, HPFCutoff, position, occlusionMetric, emitterType, environmentalLoudness);
}
}
void audRadioStation::MuteEmitters()
{
for(u8 trackIndex=0; trackIndex<2; trackIndex++)
{
m_Tracks[trackIndex].MuteEmitters();
}
}
const RadioStationTrackList::tTrack *audRadioStation::GetTrack(const s32 category, const u32 context, const s32 trackIndex, bool ignoreLocking) const
{
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(category);
#if RSG_PC
if(category == RADIO_TRACK_CAT_MUSIC && m_IsUserStation)
{
return trackLists->GetTrack(0);
}
#endif
if(!trackLists)
{
return NULL;
}
return trackLists->GetTrack(trackIndex, context, ignoreLocking);
}
void audRadioStation::AddActiveTrackToHistory()
{
bool disableNetworkModeForStation = m_UseRandomizedStrideSelection;
WIN32PC_ONLY(disableNetworkModeForStation = m_IsUserStation);
if(!disableNetworkModeForStation && (IsNetworkModeHistoryActive() || PARAM_disableradiohistory.Get()))
{
return;
}
if(m_Tracks[m_ActiveTrackIndex].IsInitialised())
{
const s32 category = m_Tracks[m_ActiveTrackIndex].GetCategory();
// only on player station...
if(category == RADIO_TRACK_CAT_NEWS)
{
if(g_RadioAudioEntity.IsPlayerRadioActive() &&
g_RadioAudioEntity.GetPlayerRadioStation() == this)
{
const u32 newsStoryId = m_Tracks[m_ActiveTrackIndex].GetTrackIndex();
naAssertf(newsStoryId < sm_NewsTrackLists.ComputeNumTracks(), "Track index is out of bounds");
if(sm_NewsStoryState[newsStoryId] == (RADIO_NEWS_ONCE_ONLY|RADIO_NEWS_UNLOCKED))
{
naDisplayf("Locking news story %u", newsStoryId+1);
sm_NewsStoryState[newsStoryId] = RADIO_NEWS_ONCE_ONLY;
}
// Generic news plays sequentially, so update the last played index
if((sm_NewsStoryState[newsStoryId] & RADIO_NEWS_ONCE_ONLY) == 0 && sm_NewsHistory[0] != static_cast<u32>(m_Tracks[m_ActiveTrackIndex].GetTrackIndex()))
{
audDisplayf("Generic news story - history index was %d, now %d", sm_NewsHistory[0] + 1, newsStoryId + 1);
sm_NewsHistory[0] = m_Tracks[m_ActiveTrackIndex].GetTrackIndex();
}
}
// News doesn't rely on normal history tracking - if the player isn't explicitly listening to this track then we don't want to lock it
return;
}
if(m_UseRandomizedStrideSelection || (category == RADIO_TRACK_CAT_MUSIC && m_PlaySequentialMusicTracks))
{
// we update the sequential index when we pick a track
return;
}
else
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(category));
if(history)
{
u32 trackId = m_Tracks[m_ActiveTrackIndex].GetRefForHistory();
#if RSG_PC // user music - this was tracked differently in GTA IV this may need fixed SORR
if(m_IsUserStation && category == RADIO_TRACK_CAT_MUSIC)
{
trackId = m_Tracks[m_ActiveTrackIndex].GetTrackIndex();
}
#endif
//Make sure we didn't already add this track.
if(history->GetPreviousEntry() != trackId)
{
history->AddToHistory(trackId);
}
}
}
}
}
const audRadioStationHistory *audRadioStation::FindHistoryForCategory(const s32 category) const
{
#if RSG_PC
if(m_IsUserStation && category == RADIO_TRACK_CAT_MUSIC)
{
return sm_UserRadioTrackManager.GetHistory();
}
#endif
// We use a single history per station for network synched radio
if(!m_UseRandomizedStrideSelection && (PARAM_disableradiohistory.Get() || IsNetworkModeHistoryActive()))
{
return &m_MusicTrackHistory;
}
switch(category)
{
case RADIO_TRACK_CAT_WEATHER:
return &sm_WeatherHistory;
case RADIO_TRACK_CAT_ADVERTS:
return &sm_AdvertHistory;
case RADIO_TRACK_CAT_MUSIC:
return &m_MusicTrackHistory;
case RADIO_TRACK_CAT_TAKEOVER_MUSIC:
// NOTE: We dont support network sync for Takeover music in Sequential mode; this is purely to allow us to capture
// the station in sequential mode, and isnt expected to be used in final.
if(m_PlaySequentialMusicTracks)
{
return &m_TakeoverMusicTrackHistory;
}
else if (m_HasTakeoverContent || m_UseRandomizedStrideSelection)
{
return &m_TakeoverMusicTrackHistory;
}
else
{
return &m_MusicTrackHistory;
}
case RADIO_TRACK_CAT_IDENTS:
return &m_IdentTrackHistory;
case RADIO_TRACK_CAT_TAKEOVER_IDENTS:
return &m_TakeoverIdentTrackHistory;
case RADIO_TRACK_CAT_DJSOLO:
return &m_DjSoloTrackHistory;
case RADIO_TRACK_CAT_TAKEOVER_DJSOLO:
return &m_TakeoverDjSoloTrackHistory;
case RADIO_TRACK_CAT_TO_AD:
return &m_ToAdTrackHistory;
case RADIO_TRACK_CAT_TO_NEWS:
return &m_ToNewsTrackHistory;
case RADIO_TRACK_CAT_USER_INTRO:
return &m_UserIntroTrackHistory;
case RADIO_TRACK_CAT_USER_OUTRO:
return &m_UserOutroTrackHistory;
case RADIO_TRACK_CAT_INTRO_MORNING:
return &m_UserIntroMorningTrackHistory;
case RADIO_TRACK_CAT_INTRO_EVENING:
return &m_UserIntroEveningTrackHistory;
case RADIO_TRACK_CAT_NEWS:
default:
audAssertf(false, "Invalid track category %u", category);
return NULL;
}
}
bool audRadioStation::IsTrackInHistory(const u32 trackId, const s32 category, const s32 historyLength) const
{
const audRadioStationHistory *history = FindHistoryForCategory(category);
if(history)
{
const bool ret = history->IsInHistory(trackId, historyLength);
#if __BANK
if(g_DebugRadioRandom && ret)
{
naDebugf1("%s - skipping track id %08X (%s) due to history", GetName(), trackId, TrackCats_ToString((TrackCats)category));
}
#endif
return ret;
}
return false;
}
bool audRadioStation::IsLocked() const
{
bool stationLocked = AUD_GET_TRISTATE_VALUE(m_StationSettings->Flags, FLAG_ID_RADIOSTATIONSETTINGS_LOCKED) == AUD_TRISTATE_TRUE;
return stationLocked;
}
s32 audRadioStation::GetRandomNumberInRange(const s32 min, const s32 max)
{
BANK_ONLY(u32 seed = m_Random.GetSeed());
s32 ret = m_Random.GetRanged(min, max);
#if __BANK
if(g_DebugRadioRandom)
naDebugf1("%s - GetRandomNumberInRange(%d, %d) - %d (seed was %u)", GetName(), min, max, ret, seed);
#endif
return ret;
}
float audRadioStation::GetRandomNumberInRange(const float min, const float max)
{
BANK_ONLY(u32 seed = m_Random.GetSeed());
float ret = m_Random.GetRanged(min, max);
#if __BANK
if(g_DebugRadioRandom)
naDebugf1("%s - GetRandomNumberInRange(%f, %f) - %f (seed was %u)", GetName(), min, max, ret, seed);
#endif
return ret;
}
bool audRadioStation::ResolveProbability(const float Probability)
{
if (Probability >= 1.0f)
{
#if __BANK
if(g_DebugRadioRandom)
naDebugf1("%s - ResolveProbability(%f) - true", GetName(), Probability);
#endif
return true;
}
float r = GetRandomNumberInRange(0.0f, 1.0f);
#if __BANK
if(g_DebugRadioRandom)
naDebugf1("%s - ResolveProbability(%f) - %f (%s)", GetName(), Probability, r, r < Probability ? "true" : "false");
#endif
return (r < Probability);
}
bool audRadioStation::IsValidCategoryForStation(const TrackCats category, const bool playNews) const
{
switch(category)
{
case RADIO_TRACK_CAT_NEWS:
return playNews;
case RADIO_TRACK_CAT_WEATHER:
return m_PlayWeather;
case RADIO_TRACK_CAT_INTRO_MORNING:
case RADIO_TRACK_CAT_INTRO_EVENING:
#if RSG_PC
if(m_IsUserStation && m_MusicRunningCount > 1 && sm_UserRadioTrackManager.GetRadioMode() == USERRADIO_PLAY_MODE_RADIO)
{
const s32 gameHours = CClock::GetHour();
if(category == RADIO_TRACK_CAT_INTRO_MORNING)
{
return (gameHours >= (s32)g_RadioDjTimeMorningStart && gameHours < (s32)g_RadioDjTimeMorningEnd);
}
else
{
// Evening
return (gameHours >= (s32)g_RadioDjTimeEveningStart && gameHours < (s32)g_RadioDjTimeEveningEnd);
}
}
#endif
return false;
case RADIO_TRACK_CAT_USER_INTRO:
#if RSG_PC
return m_IsUserStation && m_MusicRunningCount > 1 && sm_UserRadioTrackManager.GetRadioMode() == USERRADIO_PLAY_MODE_RADIO;
#else
return false;
#endif // RSG_PC
case RADIO_TRACK_CAT_TO_AD:
case RADIO_TRACK_CAT_TO_NEWS:
#if RSG_PC
return m_IsUserStation && m_MusicRunningCount > 1 && sm_UserRadioTrackManager.GetRadioMode() == USERRADIO_PLAY_MODE_RADIO;
#else
return false;
#endif // RSG_PC
case RADIO_TRACK_CAT_USER_OUTRO:
#if RSG_PC
// avoid outro after only one song.
return m_IsUserStation && m_MusicRunningCount > 1 && sm_UserRadioTrackManager.GetRadioMode() == USERRADIO_PLAY_MODE_RADIO;
#else
return false;
#endif
case RADIO_TRACK_CAT_MUSIC:
#if RSG_PC
// prevent runs longer than 3 songs
// TODO: factor in time
if(m_IsUserStation && sm_UserRadioTrackManager.GetRadioMode() == USERRADIO_PLAY_MODE_RADIO && m_MusicRunningCount >= 3)
{
return false;
}
#endif
return true;
case RADIO_TRACK_CAT_DJSOLO:
case RADIO_TRACK_CAT_IDENTS:
#if RSG_PC
if(m_IsUserStation)
{
return sm_UserRadioTrackManager.GetRadioMode() == USERRADIO_PLAY_MODE_RADIO && m_MusicRunningCount > 1;
}
#endif
return true;
case RADIO_TRACK_CAT_TAKEOVER_DJSOLO:
case RADIO_TRACK_CAT_TAKEOVER_IDENTS:
case RADIO_TRACK_CAT_TAKEOVER_MUSIC:
return m_HasTakeoverContent;
default:
return true;
}
}
u8 audRadioStation::ComputeNextTrackCategoryForTakeoverStation(u32 timeInMs, u32 activeTrackCategory)
{
audDisplayf("Takeover station calculating next category at %ums, active track category %u, seed %u, takeover time %u, music count %u", timeInMs, activeTrackCategory, m_Random.GetSeed(), m_TakeoverLastTimeMs, m_MusicRunningCount);
u8 nextTrackCategory = RADIO_TRACK_CAT_MUSIC;
switch(activeTrackCategory)
{
case RADIO_TRACK_CAT_MUSIC:
// Should we go into a takeover state?
if(timeInMs >= m_TakeoverLastTimeMs + m_TakeoverMinTimeMs && ResolveProbability(m_TakeoverProbability))
{
nextTrackCategory = RADIO_TRACK_CAT_TAKEOVER_IDENTS;
}
else
{
// check music running counter
if(m_MusicRunningCount > 2 || (m_MusicRunningCount>1&&ResolveProbability(0.35f)))
{
nextTrackCategory = RADIO_TRACK_CAT_IDENTS;
}
else
{
nextTrackCategory = RADIO_TRACK_CAT_MUSIC;
}
}
break;
case RADIO_TRACK_CAT_IDENTS:
// DJSOLOs are only going to follow IDENTs on this station
nextTrackCategory = (u8)(ResolveProbability(0.75f) ? RADIO_TRACK_CAT_DJSOLO : RADIO_TRACK_CAT_MUSIC);
break;
case RADIO_TRACK_CAT_TAKEOVER_IDENTS:
if(ResolveProbability(m_TakeoverDjSoloProbability))
{
nextTrackCategory = RADIO_TRACK_CAT_TAKEOVER_DJSOLO;
}
else
{
nextTrackCategory = RADIO_TRACK_CAT_TAKEOVER_MUSIC;
}
break;
case RADIO_TRACK_CAT_TAKEOVER_DJSOLO:
nextTrackCategory = RADIO_TRACK_CAT_TAKEOVER_MUSIC;
break;
case RADIO_TRACK_CAT_TAKEOVER_MUSIC:
if(m_TakeoverMusicTrackCounter == 0)
{
// End the takeover
nextTrackCategory = RADIO_TRACK_CAT_IDENTS;
}
else
{
nextTrackCategory = RADIO_TRACK_CAT_TAKEOVER_MUSIC;
}
break;
//case g_NullRadioTrackCategory: - Start on music.
default:
//Transition to music only.
nextTrackCategory = RADIO_TRACK_CAT_MUSIC;
break;
}
audDisplayf("Takeover station calculated next category as %s", TrackCats_ToString((TrackCats)nextTrackCategory));
return nextTrackCategory;
}
bool audRadioStation::PlayIdentsInsteadOfAds() const
{
// Special case, we want to play idents instead of adverts on West Coast Classics during the launch event. Safer to link this to the actual track list collection
// being unlocked rather than the tuneable so that we can ensure we stay in sync with whatever script is doing in terms of maintaining the station's SP/MP state
if(m_NameHash == ATSTRINGHASH("RADIO_09_HIPHOP_OLD", 0x572C04) && IsTrackListUnlocked(ATSTRINGHASH("RADIO_09_HIPHOP_OLD_DD_MUSIC_LAUNCH", 0x2018C9AF)))
{
return true;
}
return m_PlayIdentsInsteadOfAds;
}
u8 audRadioStation::ComputeNextTrackCategory(u32 timeInMs)
{
u8 nextTrackCategory = 0;
const RadioTrackCategoryData *weights;
f32 weightSum = 0.0f;
f32 r = 0.0f;
if(m_ForceNextTrackCategory != 0xff)
{
const u8 ret = m_ForceNextTrackCategory;
m_ForceNextTrackCategory = 0xff;
return ret;
}
#if RSG_PC
if(m_BadTrackCount > 4)
{
Warningf("Forcing an advert as bad track count is > 4");
m_ShouldPlayFullRadio = true;
sm_IsInRecoveryMode = true;
return RADIO_TRACK_CAT_ADVERTS;
}
if(sm_IsInRecoveryMode)
{
// keep trying to play music until we are either successful or bad track count hits
// the limit
return RADIO_TRACK_CAT_MUSIC;
}
#endif
u32 activeTrackCategory = m_Tracks[m_ActiveTrackIndex].GetCategory();
if(m_OnlyPlayAds)
{
//@@: location AUDRADIOSTATION_COMPUTENEXTTRACKCATEGORY_RETURN_ADVERTISEMENTS
return RADIO_TRACK_CAT_ADVERTS;
}
if(ShouldOnlyPlayMusic())
{
return RADIO_TRACK_CAT_MUSIC;
}
// see if there are any unlocked mission stories; no back to back news however
// only for player station
bool hasUnlockedNews = false;
if(!IsNetworkModeHistoryActive())
{
for(u32 i = 0; i < kMaxNewsStories; i++)
{
if(sm_NewsStoryState[i] == (RADIO_NEWS_UNLOCKED|RADIO_NEWS_ONCE_ONLY))
{
hasUnlockedNews = true;
break;
}
}
}
// only play news on the player station
const bool playNews = m_PlayNews && !IsNetworkModeHistoryActive() && !PARAM_disableradiohistory.Get() &&
g_RadioAudioEntity.IsPlayerRadioActive() && g_RadioAudioEntity.GetPlayerRadioStation() == this;
if(playNews && activeTrackCategory != RADIO_TRACK_CAT_NEWS &&
hasUnlockedNews &&
timeInMs >= GetNextValidSelectionTime(RADIO_TRACK_CAT_NEWS))
{
m_HasJustPlayedBackToBackMusic = false;
m_HasJustPlayedBackToBackAds = false;
#if RSG_PC
if(m_IsUserStation && activeTrackCategory != RADIO_TRACK_CAT_TO_NEWS)
{
return RADIO_TRACK_CAT_TO_NEWS;
}
#endif
return RADIO_TRACK_CAT_NEWS;
}
#if RSG_PC
if(m_IsUserStation && !HasUserTracks() && activeTrackCategory == RADIO_TRACK_CAT_IDENTS)
{
// act like we have just played music so we can transition to anything
activeTrackCategory = RADIO_TRACK_CAT_MUSIC;
}
#endif
if(m_HasTakeoverContent)
{
nextTrackCategory = ComputeNextTrackCategoryForTakeoverStation(m_StationAccumulatedPlayTimeMs, activeTrackCategory);
}
else
{
switch(activeTrackCategory)
{
case RADIO_TRACK_CAT_MUSIC:
//Transition to any category.
if(m_NoBackToBackMusic && activeTrackCategory == RADIO_TRACK_CAT_MUSIC)
{
// we just played a music track so don't want to allow another music track
weights = sm_RadioTrackCategoryWeightsNoBackToBackMusic;
}
else if(m_HasJustPlayedBackToBackMusic)
{
//We already played back-to-back music, so prevent another music track (or ident->music) being chosen.
weights = m_NameHash == ATSTRINGHASH("RADIO_34_DLC_HEI4_KULT", 0xE3442163) ? sm_RadioTrackCategoryWeightsFromBackToBackMusicKult : sm_RadioTrackCategoryWeightsFromBackToBackMusic;
}
else
{
weights = m_NameHash == ATSTRINGHASH("RADIO_34_DLC_HEI4_KULT", 0xE3442163) ? sm_RadioTrackCategoryWeightsNoBackToBackMusicKult : sm_RadioTrackCategoryWeightsFromMusic;
}
#if RSG_PC
if(m_IsUserStation)
{
weights = sm_UserMusicWeightsFromMusic;
// Default to music if we've temporarily exhausted all other valid categories.
nextTrackCategory = RADIO_TRACK_CAT_MUSIC;
}
#endif
//Compute sum of weights; only for valid track cats.
{
#if RSG_PC
const u32 lastCat = m_IsUserStation ? RADIO_TRACK_CAT_INTRO_EVENING : RADIO_TRACK_CAT_DJSOLO;
#else
const u32 lastCat = RADIO_TRACK_CAT_DJSOLO;
#endif
bool validTimeForCategory = true;
for(u32 i = 0; i <= lastCat; i++) // TODO: re-enable new categories after audio sync
{
if(weights)
{
#if RSG_PC
if(m_IsUserStation)
{
validTimeForCategory = timeInMs >= GetNextValidSelectionTime(weights->Category[i].Category);
}
#endif
if(IsValidCategoryForStation((TrackCats)weights->Category[i].Category, playNews) && validTimeForCategory)
{
f32 modifiedWeight = (float)weights->Category[i].Value;
weightSum += modifiedWeight;
}
}
}
//Compute random r in range [0.0f, weightSum].
r = GetRandomNumberInRange(0.0f, weightSum);
//Find where r falls in weight range to pick category.
for(u32 i = 0; i <= lastCat; i++) // TODO: re-enable new categories after audio sync
{
if(weights)
{
#if RSG_PC
if(m_IsUserStation)
{
validTimeForCategory = timeInMs >= GetNextValidSelectionTime(weights->Category[i].Category);
}
#endif
if(IsValidCategoryForStation((TrackCats)weights->Category[i].Category, playNews) && validTimeForCategory)
{
f32 modifiedWeight = (float)weights->Category[i].Value;
r -= modifiedWeight;
if((weights->Category[i].Value > 0.0f) && (r <= 0.0f))
{
nextTrackCategory = weights->Category[i].Category;
break;
}
}
}
}
}
// only choose news if we don't have any unlocked mission stories waiting to go (since we only want to play them on
// the player station; picking news here will mean we don't play another news for a few mins even if this plays virtually)
if(nextTrackCategory == RADIO_TRACK_CAT_NEWS && hasUnlockedNews)
{
RADIODEBUG("Playing an advert instead of news since not player station and there is a pending mission news story");
nextTrackCategory = RADIO_TRACK_CAT_ADVERTS;
}
else
{
//Ensure that we don't hear news and weather too often on this station.
if((nextTrackCategory == RADIO_TRACK_CAT_NEWS) ||
(nextTrackCategory == RADIO_TRACK_CAT_WEATHER))
{
if(timeInMs < GetNextValidSelectionTime(nextTrackCategory))
{
//We heard this category too recently, so play something else instead.
#if RSG_PC
if(m_IsUserStation)
{
BANK_ONLY(audDisplayf("%s - falling back to music rather than %s due to selection time", GetName(), GetTrackCategoryName(nextTrackCategory)));
nextTrackCategory = RADIO_TRACK_CAT_MUSIC;
}
else
#endif
nextTrackCategory = RADIO_TRACK_CAT_ADVERTS;
}
}
}
break;
case RADIO_TRACK_CAT_INTRO_MORNING:
case RADIO_TRACK_CAT_INTRO_EVENING:
case RADIO_TRACK_CAT_USER_INTRO:
nextTrackCategory = RADIO_TRACK_CAT_MUSIC;
break;
case RADIO_TRACK_CAT_TO_AD:
nextTrackCategory = RADIO_TRACK_CAT_ADVERTS;
break;
case RADIO_TRACK_CAT_TO_NEWS:
nextTrackCategory = RADIO_TRACK_CAT_NEWS;
break;
case RADIO_TRACK_CAT_USER_OUTRO:
nextTrackCategory = RADIO_TRACK_CAT_IDENTS;
break;
case RADIO_TRACK_CAT_IDENTS:
//Transition to music only.
nextTrackCategory = RADIO_TRACK_CAT_MUSIC;
break;
case RADIO_TRACK_CAT_ADVERTS:
// No back-to-back adverts in the network game to maximise history usage for songs
if(!IsNetworkModeHistoryActive() && m_PlaysBackToBackAds && !m_HasJustPlayedBackToBackAds && ResolveProbability(0.25f))
{
nextTrackCategory = RADIO_TRACK_CAT_ADVERTS;
}
else
{
nextTrackCategory = RADIO_TRACK_CAT_IDENTS;
}
break;
case RADIO_TRACK_CAT_NEWS:
case RADIO_TRACK_CAT_WEATHER:
nextTrackCategory = RADIO_TRACK_CAT_IDENTS;
break;
//case g_NullRadioTrackCategory: - Start on music.
default:
//Transition to music only.
nextTrackCategory = RADIO_TRACK_CAT_MUSIC;
#if RSG_PC // user music
if(m_IsUserStation && activeTrackCategory != RADIO_TRACK_CAT_DJSOLO && ResolveProbability(0.5f))
{
nextTrackCategory = RADIO_TRACK_CAT_USER_INTRO;
}
#endif
}
}
if(nextTrackCategory == RADIO_TRACK_CAT_ADVERTS && PlayIdentsInsteadOfAds())
{
nextTrackCategory = RADIO_TRACK_CAT_IDENTS;
}
if(m_NameHash == ATSTRINGHASH("RADIO_37_MOTOMAMI", 0x97074FCC))
{
if(m_MusicRunningCount < 2)
{
nextTrackCategory = RADIO_TRACK_CAT_MUSIC;
}
else if(nextTrackCategory == RADIO_TRACK_CAT_DJSOLO)
{
if(ResolveProbability(0.2f))
{
nextTrackCategory = RADIO_TRACK_CAT_IDENTS;
}
}
}
m_HasJustPlayedBackToBackMusic = (activeTrackCategory == RADIO_TRACK_CAT_MUSIC) && (nextTrackCategory == RADIO_TRACK_CAT_MUSIC);
m_HasJustPlayedBackToBackAds = (activeTrackCategory == RADIO_TRACK_CAT_ADVERTS && nextTrackCategory == RADIO_TRACK_CAT_ADVERTS);
// Update this along with m_TakeoverMusicTrackCounter for takeover stations
if(!m_HasTakeoverContent)
{
if(nextTrackCategory == RADIO_TRACK_CAT_MUSIC)
{
m_MusicRunningCount++;
}
else
{
m_MusicRunningCount = 0;
}
}
return nextTrackCategory;
}
const RadioStationTrackList::tTrack *audRadioStation::ComputeRandomTrack(s32 &category, s32 &index)
{
naCErrorf(category < NUM_RADIO_TRACK_CATS, "Category for random track (%d) is invalid", category);
if(/* m_ForceNextTrackContext != kNullContext && */ m_ForceNextTrackIndex != 0xffff)
{
naDisplayf("Forcing track index %u context %u category %d", m_ForceNextTrackIndex, m_ForceNextTrackContext, category);
const RadioStationTrackList::tTrack *ret = GetTrack(category, m_ForceNextTrackContext, m_ForceNextTrackIndex, true);
Assign(index, m_ForceNextTrackIndex);
m_ForceNextTrackIndex = 0xffff;
m_ForceNextTrackContext = kNullContext;
return ret;
}
u32 requiredContext = kNullContext;
if(m_ForceNextTrackContext != kNullContext)
{
requiredContext = m_ForceNextTrackContext;
m_ForceNextTrackContext = kNullContext;
}
else
{
//Deal with custom News and Weather contexts.
const bool isNightTime = (CClock::GetHour() > 18 || CClock::GetHour() < 6);
const CWeatherType& weatherType = (g_weather.GetInterp() > 0.5f ? g_weather.GetNextType() : g_weather.GetPrevType());
if(category == RADIO_TRACK_CAT_WEATHER)
{
//@@: location AUDRADIOSTATION_COMPUTERANDOMTRACK_ON_WEATHER_TRACK
if(weatherType.m_fog>0.5f)
{
requiredContext = g_RadioTrackWeatherContextNames[RADIO_TRACK_WEATHER_CONTEXT_FOG];
}
else if(weatherType.m_rain>0.0f)
{
requiredContext = g_RadioTrackWeatherContextNames[RADIO_TRACK_WEATHER_CONTEXT_RAIN];
}
else if(!isNightTime && (weatherType.m_sun>0.6f))
{
requiredContext = g_RadioTrackWeatherContextNames[RADIO_TRACK_WEATHER_CONTEXT_SUN];
}
else if(weatherType.m_lightning)
{
requiredContext = g_RadioTrackWeatherContextNames[RADIO_TRACK_WEATHER_CONTEXT_WIND];
}
// inhibit cloudy reports at night
else if(!isNightTime && weatherType.m_cloud>0.7f)
{
requiredContext = g_RadioTrackWeatherContextNames[RADIO_TRACK_WEATHER_CONTEXT_CLOUD];
}
else
{
//We don't support the current weather type, so play an advert instead.
RADIODEBUG("Current weather type unsupported; playing advert");
category = RADIO_TRACK_CAT_ADVERTS;
}
}
}
u32 numTracks = 0;
const bool isNetworkGameInProgress = IsNetworkModeHistoryActive();
// Don't play mission news in the network game
if(category == RADIO_TRACK_CAT_NEWS)
{
numTracks = 0;
if(!isNetworkGameInProgress && !PARAM_disableradiohistory.Get())
{
// see if there are any unlocked mission stories
u32 firstGenericStory = ~0U;
u32 lastPlayedGenericNewsStory = sm_NewsHistory[0];
u32 nextGenericNewsStory = ~0U;
for(s32 i = 0; i < kMaxNewsStories; i++)
{
if(sm_NewsStoryState[i] & RADIO_NEWS_UNLOCKED && (sm_NewsStoryState[i]&RADIO_NEWS_ONCE_ONLY) == 0)
{
// Find the earliest unlocked generic story
u32 storyId = (u32)i;
if(storyId < firstGenericStory)
{
firstGenericStory = storyId;
}
// Find the first unlocked generic story after the last one played
if(storyId > lastPlayedGenericNewsStory && nextGenericNewsStory == ~0U)
{
nextGenericNewsStory = storyId;
}
}
if(sm_NewsStoryState[i] == (RADIO_NEWS_UNLOCKED|RADIO_NEWS_ONCE_ONLY))
{
// relock this story only when played phys on the player station
index = i;
return GetTrack(category, kNullContext, i);
}
}
const u32 numNewsStoriesAvailable = sm_NewsTrackLists.ComputeNumTracks();
if(nextGenericNewsStory >= numNewsStoriesAvailable)
{
nextGenericNewsStory = firstGenericStory;
}
if(nextGenericNewsStory < numNewsStoriesAvailable)
{
index = (s32)nextGenericNewsStory;
return GetTrack(category, kNullContext, index);
}
}// !isNetworkGameInProgress
// else - fall back to something else below
}
else
{
numTracks = ComputeNumTracksAvailable(category, requiredContext);
}
//audRadioStationTrackInfo &trackInfo = m_TrackInfoForCategory[*category];
if(numTracks == 0)
{
//We don't have any tracks for this category/context...
switch(category)
{
case RADIO_TRACK_CAT_IDENTS:
RADIODEBUG("Failed to get ident; falling back to music");
category = RADIO_TRACK_CAT_MUSIC;
numTracks = ComputeNumTracksAvailable(category);
break;
case RADIO_TRACK_CAT_NEWS:
case RADIO_TRACK_CAT_WEATHER: // "
case RADIO_TRACK_CAT_DJSOLO:
//Try and play an advert instead.
RADIODEBUG2("Wanted %s, falling back to advert", TrackCats_ToString((TrackCats)category));
category = RADIO_TRACK_CAT_ADVERTS;
numTracks = ComputeNumTracksAvailable(category);
if(numTracks == 0)
{
//We should always have music as a fallback.
RADIODEBUG("Failed to get advert; falling back to music");
category = RADIO_TRACK_CAT_MUSIC;
numTracks = ComputeNumTracksAvailable(category);
}
break;
case RADIO_TRACK_CAT_MUSIC:
#if RSG_PC // user music
if(m_IsUserStation)
{
// Fall back to advert
category = RADIO_TRACK_CAT_ADVERTS;
numTracks = ComputeNumTracksAvailable(category);
}
else if(!IsUSBMixStation())
{
naAssertf(numTracks > 0, "No music tracks for radio station %s", GetName());
}
#endif
return NULL;
//case RADIO_TRACK_CAT_ADVERTS:
default:
//We should always have music as a fallback.
category = RADIO_TRACK_CAT_MUSIC;
numTracks = ComputeNumTracksAvailable(category);
}
requiredContext = kNullContext;
}
naCErrorf(IsUSBMixStation() || numTracks > 0, "No available tracks for category %d in ComputeRandomTrack", category);
const RadioStationTrackList::tTrack *track = NULL;
if((category == RADIO_TRACK_CAT_MUSIC || category == RADIO_TRACK_CAT_TAKEOVER_MUSIC) && m_PlaySequentialMusicTracks)
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(category));
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(category);
if(naVerifyf(history && trackLists, "Failed to get history or track lists for category RADIO_TRACK_CAT_MUSIC"))
{
// need this mod here in case we've loaded a save game that had a different number of tracks
const s32 numTracks = trackLists->ComputeNumTracks();
if (numTracks > 0)
{
u32 trackIndexToPlay = (*history)[0] % numTracks;
(*history)[0] = (trackIndexToPlay + 1) % numTracks;
history->SetWriteIndex(1);
track = trackLists->GetTrack(trackIndexToPlay);
naCErrorf(track, "Failed to get track for category RADIO_TRACK_CAT_MUSIC at index %d", trackIndexToPlay);
index = trackIndexToPlay;
}
}
}
else if(m_UseRandomizedStrideSelection && IsRandomizedStrideCategory(category))
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(category));
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(category);
// If we're using takeover mode or we only have one set of data (rather than old/new data) then we can just do a simple
// selection from any of the tracks
if(m_HasTakeoverContent || trackLists->GetUnlockedListCount() == 1)
{
if(naVerifyf(history && trackLists, "Failed to get history or track lists for category %s", TrackCats_ToString((TrackCats)category)))
{
const s32 numTracks = trackLists->ComputeNumTracks();
const u32 previousTrack = history->GetPreviousEntry();
u32 trackIndexToPlay = (previousTrack + numTracks + m_CategoryStride[category]) % numTracks;
(*history)[0] = trackIndexToPlay;
history->SetWriteIndex(1);
track = trackLists->GetTrack(trackIndexToPlay);
naCErrorf(track, "Failed to get track for category %s at index %d", TrackCats_ToString((TrackCats)category), trackIndexToPlay);
audDisplayf("Setting category %s to %u (num tracks: %u)", TrackCats_ToString((TrackCats)category), trackIndexToPlay, numTracks);
index = trackIndexToPlay;
}
}
// If we have old/new data then we need to first select which track list to play from and then pick which track from that track list
// to play, using the per-track list stride/history (stored in the takeover state slot for the new data)
else if(audVerifyf(trackLists->GetUnlockedListCount() == 2, "Station %s is in stride mode with %d unlocked track lists", GetName(), trackLists->GetUnlockedListCount()))
{
u32 numTracks = 0;
u32 numNewTracks = 0;
u32 unlockedListIndex = 0;
for(u32 i = 0; i < trackLists->GetListCount(); i++)
{
if(trackLists->IsTrackListUnlocked(trackLists->GetList(i)))
{
const u32 thisTrackListNumTracks = trackLists->GetList(i)->numTracks;
numTracks += thisTrackListNumTracks;
if(unlockedListIndex == 1)
{
numNewTracks = thisTrackListNumTracks;
}
unlockedListIndex++;
}
}
// work out probability of picking new track based on normal distribution
const float normalProbability = numNewTracks / static_cast<float>(numTracks);
const float scaledProbability = normalProbability * (m_IsFirstMusicTrackSinceBoot ? m_FirstBootNewTrackBias : m_NewTrackBias);
m_IsFirstMusicTrackSinceBoot = false;
u32 trackListIndex = ResolveProbability(scaledProbability) ? 1 : 0;
s32 adjustedCategory = category;
if(trackListIndex == 1)
{
if(category == RADIO_TRACK_CAT_MUSIC) { adjustedCategory = RADIO_TRACK_CAT_TAKEOVER_MUSIC; }
if(category == RADIO_TRACK_CAT_IDENTS) { adjustedCategory = RADIO_TRACK_CAT_TAKEOVER_IDENTS; }
if(category == RADIO_TRACK_CAT_DJSOLO) { adjustedCategory = RADIO_TRACK_CAT_TAKEOVER_DJSOLO; }
history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(adjustedCategory));
}
unlockedListIndex = 0;
for(u32 i = 0; i < trackLists->GetListCount(); i++)
{
if(trackLists->IsTrackListUnlocked(trackLists->GetList(i)))
{
if(unlockedListIndex == trackListIndex)
{
const RadioStationTrackList* trackList = trackLists->GetList(i);
const s32 numTracks = trackList->numTracks;
const u32 previousTrack = history->GetPreviousEntry();
u32 localTrackIndexToPlay = (previousTrack + numTracks + m_CategoryStride[adjustedCategory]) % numTracks;
(*history)[0] = localTrackIndexToPlay;
history->SetWriteIndex(1);
// Map our track list specific index back to a global index
u32 trackIndexToPlay = FindTrackId(category, trackList->Track[localTrackIndexToPlay].SoundRef);
track = trackLists->GetTrack(trackIndexToPlay);
naCErrorf(track, "Failed to get track for category %s at index %d", TrackCats_ToString((TrackCats)category), trackIndexToPlay);
audDisplayf("Setting category %s to %u (num tracks: %u)", TrackCats_ToString((TrackCats)category), trackIndexToPlay, numTracks);
index = trackIndexToPlay;
break;
}
unlockedListIndex++;
}
}
}
}
else
{
s32 firstTrackIndex = 0;
s32 endTrackIndex = (s32)numTracks - 1;
// favor new tracks slightly (those not in the first playlist)
if(WIN32PC_ONLY(!m_IsUserStation && )
category == RADIO_TRACK_CAT_MUSIC &&
numTracks > 0)
{
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC);
if(m_IsFirstMusicTrackSinceBoot && trackLists->GetUnlockedListCount() > 1)
{
u32 numNewTracks = 0;
// see how many new tracks we have
bool haveMultipleTrackListsAvailable = false;
for(s32 trackListIndex = 1; trackListIndex < trackLists->GetListCount(); trackListIndex++)
{
if(!trackLists->IsListLocked(trackListIndex))
{
haveMultipleTrackListsAvailable = true;
numNewTracks += trackLists->GetList(trackListIndex)->numTracks;
break;
}
}
if(haveMultipleTrackListsAvailable)
{
// work out probability of picking new track based on normal distribution
const float normalProbability = numNewTracks / static_cast<float>(numTracks);
const float scaledProbability = normalProbability * (m_IsFirstMusicTrackSinceBoot ? m_FirstBootNewTrackBias : m_NewTrackBias);
m_IsFirstMusicTrackSinceBoot = false;
if(ResolveProbability(scaledProbability))
{
audDisplayf("%s Forcing new track (first music track since boot: %s", GetName(), m_IsFirstMusicTrackSinceBoot? "TRUE" : "FALSE");
firstTrackIndex = static_cast<s32>(trackLists->GetList(0)->numTracks);
}
else
{
endTrackIndex = static_cast<s32>(trackLists->GetList(0)->numTracks - 1);
}
numTracks = (endTrackIndex - firstTrackIndex) + 1;
}
}
}
#if RSG_PC
if(m_IsUserStation && category == RADIO_TRACK_CAT_MUSIC)
{
index = sm_UserRadioTrackManager.GetNextTrack();
return GetTrack(category, requiredContext, index);
}
#endif // RSG_PC
//Make sure we don't look over too much history.
const s32 historyLength = Max<s32>((1 + endTrackIndex - firstTrackIndex) - kMinRadioTracksOutsideHistory, 0);
bool isTrackInHistory = false;
do
{
const s32 trackIndex = GetRandomNumberInRange(firstTrackIndex, endTrackIndex);
track = GetTrack(category, requiredContext, trackIndex);
index = trackIndex;
//Check that this track hasn't just been played/selected (to cover virtually streaming stations that are not adding
//selected tracks to the history.
#if RSG_PC
if(m_IsUserStation && category == RADIO_TRACK_CAT_MUSIC)
{
// Track index is used as history identifier for user music
// Should instead use hash of file path?
isTrackInHistory = IsTrackInHistory(trackIndex, category, historyLength);
}
else
#endif // RSG_PC
if(audVerifyf(track, "NULL track!"))
{
if(numTracks > 1 && audRadioTrack::MakeTrackId(category, trackIndex, track->SoundRef) == m_Tracks[m_ActiveTrackIndex].GetRefForHistory())
{
isTrackInHistory = true;
}
else
{
isTrackInHistory = IsTrackInHistory(audRadioTrack::MakeTrackId(category, trackIndex, track->SoundRef), category, historyLength);
}
}
} while(isTrackInHistory);
}
return track;
}
const audRadioStationTrackListCollection *audRadioStation::FindTrackListsForCategory(const s32 category) const
{
switch(category)
{
case RADIO_TRACK_CAT_MUSIC:
return &m_MusicTrackLists;
case RADIO_TRACK_CAT_DJSOLO:
return &m_DjSoloTrackLists;
case RADIO_TRACK_CAT_IDENTS:
return &m_IdentTrackLists;
case RADIO_TRACK_CAT_ADVERTS:
return &m_AdvertTrackLists;
case RADIO_TRACK_CAT_NEWS:
return &sm_NewsTrackLists;
case RADIO_TRACK_CAT_WEATHER:
return &sm_WeatherTrackLists;
case RADIO_TRACK_CAT_TO_AD:
return &m_ToAdTrackLists;
case RADIO_TRACK_CAT_TO_NEWS:
return &m_ToNewsTrackLists;
case RADIO_TRACK_CAT_USER_INTRO:
return &m_UserIntroTrackLists;
case RADIO_TRACK_CAT_USER_OUTRO:
return &m_UserOutroTrackLists;
case RADIO_TRACK_CAT_INTRO_MORNING:
return &m_UserIntroMorningTrackLists;
case RADIO_TRACK_CAT_INTRO_EVENING:
return &m_UserIntroEveningTrackLists;
case RADIO_TRACK_CAT_TAKEOVER_DJSOLO:
return &m_TakeoverDjSoloTrackLists;
case RADIO_TRACK_CAT_TAKEOVER_IDENTS:
return &m_TakeoverIdentTrackLists;
case RADIO_TRACK_CAT_TAKEOVER_MUSIC:
return &m_TakeoverMusicTrackLists;
default:
audAssertf(false, "Invalid category: %d", category);
return NULL;
}
}
s32 audRadioStation::ComputeNumTracksAvailable(const s32 category, u32 context) const
{
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(category);
if(!trackLists)
{
return 0;
}
#if RSG_PC
if(m_IsUserStation && category == RADIO_TRACK_CAT_MUSIC)
{
return sm_UserRadioTrackManager.GetNumTracks();
}
#endif
return trackLists->ComputeNumTracks(context);
}
void audRadioStation::ForceTrack(const char *trackSettingsName, s32 startOffsetMs)
{
naDisplayf("Forcing track %s on %s with start offset %u", trackSettingsName, GetName(), startOffsetMs);
RadioTrackSettings *trackSettings = audNorthAudioEngine::GetObject<RadioTrackSettings>(trackSettingsName);
if(trackSettings)
{
ForceTrack(trackSettings, startOffsetMs);
}
else
{
ForceTrack(atStringHash(trackSettingsName), startOffsetMs);
}
}
void audRadioStation::ForceTrack(u32 soundHash, s32 startOffsetMs)
{
u32 forcedTrackIndex = 0;
// For the battle mix stations, if we set a start offset past the end of the given track,
// we want to wrap around to the next track in the sequence
if (m_IsMixStation)
{
const RadioStationTrackList* trackList = m_MusicTrackLists.GetList(0);
if (trackList)
{
bool allTracksValid = true;
u32 selectedTrackOffset = 0;
u32 totalDuration = 0;
const StreamingSound** streamingSounds = Alloca(const StreamingSound*, trackList->numTracks);
for (u32 trackIndex = 0; trackIndex < trackList->numTracks; trackIndex++)
{
streamingSounds[trackIndex] = SOUNDFACTORY.DecompressMetadata<StreamingSound>(trackList->Track[trackIndex].SoundRef);
if (!streamingSounds[trackIndex])
{
allTracksValid = false;
break;
}
else
{
if (soundHash == trackList->Track[trackIndex].SoundRef)
{
selectedTrackOffset = totalDuration;
}
totalDuration += streamingSounds[trackIndex]->Duration;
}
}
if (allTracksValid)
{
u32 adjustedOffset = (selectedTrackOffset + startOffsetMs) % totalDuration;
u32 trackEnd = 0;
u32 trackStart = 0;
for (u32 trackIndex = 0; trackIndex < trackList->numTracks; trackIndex++)
{
trackStart = trackEnd;
trackEnd += streamingSounds[trackIndex]->Duration;
if (adjustedOffset < trackEnd)
{
startOffsetMs = adjustedOffset - trackStart;
if (soundHash != trackList->Track[trackIndex].SoundRef)
{
naDisplayf("Requested track offset is beyond requested track length - wrapping around to track index %u, offset %u", trackIndex, startOffsetMs);
}
soundHash = trackList->Track[trackIndex].SoundRef;
forcedTrackIndex = trackIndex;
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC));
(*history)[0] = (trackIndex + 1) % trackList->numTracks;
break;
}
}
}
}
}
audMetadataObjectInfo objectInfo;
if (SOUNDFACTORY.GetMetadataManager().GetObjectInfo(soundHash, objectInfo) && objectInfo.GetType() == StreamingSound::TYPE_ID)
{
RadioTrackSettings tempTrackSettings;
tempTrackSettings.Sound = soundHash;
tempTrackSettings.History.Category = RADIO_TRACK_CAT_MUSIC;
tempTrackSettings.History.Sound = soundHash;
ForceTrack(&tempTrackSettings, startOffsetMs, forcedTrackIndex);
}
else
{
audAssertf(false, "Failed to get streaming sound from hash: %d", soundHash);
}
}
void audRadioStation::ForceTrack(const RadioTrackSettings *trackSettings, s32 startOffsetMs, u32 trackIndex)
{
if(naVerifyf(IsFrozen(), "Attempting to force track on %s but the station is not frozen", GetName()))
{
if(m_Tracks[m_ActiveTrackIndex].IsInitialised())
{
m_Tracks[m_ActiveTrackIndex].Shutdown();
f32 startOffset = trackSettings->StartOffset;
if (startOffsetMs >= 0)
{
const StreamingSound *streamingSound = SOUNDFACTORY.DecompressMetadata<StreamingSound>(trackSettings->Sound);
if (streamingSound && streamingSound->Duration > 0)
{
startOffset = Min(1.f, startOffsetMs / (f32)streamingSound->Duration);
}
}
audAssertf(trackSettings->History.Category != RADIO_TRACK_CAT_NEWS, "Forced radio track set to NEWS - this is not supported");
m_Tracks[m_ActiveTrackIndex].Init( trackSettings->History.Category,
trackIndex,
trackSettings->Sound,
startOffset,
m_IsMixStation && !IsUSBMixStation(),
m_IsReverbStation,
m_NameHash
);
// Override the sound ref used for history if specified, otherwise default to the actual track sound
const u32 soundRefForHistory = trackSettings->History.Sound != 0 ? trackSettings->History.Sound : trackSettings->Sound;
const u32 trackHistoryId = FindTrackId(trackSettings->History.Category, soundRefForHistory);
if(trackHistoryId != ~0U)
{
m_Tracks[m_ActiveTrackIndex].SetRefForHistory(audRadioTrack::MakeTrackId(trackSettings->History.Category, trackHistoryId, soundRefForHistory));
}
// We need to support forcing a track on a station that isn't playing physically, in which case don't forcibly prepare
// the track.
if(m_ShouldStreamPhysically)
{
Assert(m_WaveSlots[m_ActiveTrackIndex]);
m_Tracks[m_ActiveTrackIndex].Prepare(0);
}
m_Tracks[m_ActiveTrackIndex].PlayWhenReady();
if(m_UseRandomizedStrideSelection)
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(trackSettings->History.Category));
if(history)
{
(*history)[0] = trackHistoryId;
history->SetWriteIndex(1);
}
}
#if RSG_PC
m_LastPlayedTrackIndex = m_CurrentlyPlayingTrackIndex;
m_CurrentlyPlayingTrackIndex = m_Tracks[m_ActiveTrackIndex].GetTrackIndex();
#endif
// Also shutdown the next track if we've already initialised it, to ensure we haven't already queued the same track
u32 nextIndex = (m_ActiveTrackIndex+1) & 1;
if(m_Tracks[nextIndex].IsInitialised())
{
m_Tracks[nextIndex].Shutdown();
}
}
else
{
audWarningf("Attempting to force radio track, but active track is not initialised");
}
}
}
u32 audRadioStation::FindTrackId(const s32 category, const u32 soundRef) const
{
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(category);
u32 numTracks = 0;
if(trackLists)
{
numTracks = trackLists->ComputeNumTracks();
for(u32 i = 0 ; i < numTracks; i++)
{
if(soundRef == trackLists->GetTrack(i)->SoundRef)
{
return i;
}
}
}
return ~0U;
}
audPrepareState audRadioStation::PrepareNextTrack()
{
audPrepareState prepareState = AUD_PREPARE_FAILED;
u32 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
audRadioTrack &nextTrack = m_Tracks[nextTrackIndex];
nextTrack.SetPhysicalStreamingState(m_ShouldStreamPhysically, m_WaveSlots[nextTrackIndex], m_SoundBucketId);
if(nextTrack.IsInitialised())
{
//Sanity-check that we are preparing a track of the correct category.
//@@: location AUDRADIOSTATION_PREPARENEXTTRACK_SANITY_CHECK
if(ShouldOnlyPlayMusic() && nextTrack.GetCategory() != RADIO_TRACK_CAT_MUSIC)
{
//We should only choose music.
Assert(!IsFrozen());
//Stop preparing this track and allow a music track to be selected next frame.
nextTrack.Shutdown();
return AUD_PREPARING;
}
//We are already in the process of preparing this track, so query it directly.
if(m_ShouldStreamPhysically)
{
audAssertf(nextTrack.GetPlayTime() >= 0, "Negative start offset on radio track %s: %d ", GetName(), nextTrack.GetPlayTime());
s32 playTime = nextTrack.GetPlayTime() >= 0 ? nextTrack.GetPlayTime() : 0;
prepareState = nextTrack.Prepare(playTime);
}
else
{
prepareState = AUD_PREPARED;
}
}
else
{
prepareState = AUD_PREPARING;
}
return prepareState;
}
bool audRadioStation::InitialiseNextTrack(const u32 timeInMs)
{
if(g_RadioAudioEntity.IsInControlOfRadio())
{
u32 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
audRadioTrack &nextTrack = m_Tracks[nextTrackIndex];
s32 category = RADIO_TRACK_CAT_MUSIC;
s32 index = 0;
const RadioStationTrackList::tTrack *trackMetadata = NULL;
bool wasNetworkSelectedTrack = false;
if(m_QueuedTrackList)
{
trackMetadata = &m_QueuedTrackList->Track[m_QueuedTrackListIndex++];
if(m_QueuedTrackListIndex >= m_QueuedTrackList->numTracks)
{
m_QueuedTrackList = NULL;
m_QueuedTrackListIndex = 0;
}
}
else if(m_NetworkNextTrack.category != 0xff)
{
trackMetadata = GetTrack(m_NetworkNextTrack.category, kNullContext, m_NetworkNextTrack.index);
index = m_NetworkNextTrack.index;
category = m_NetworkNextTrack.category;
m_NetworkNextTrack.category = 0xff;
// For sequential music stations, ensure consistency by configuring the history state to be the same as if this
// track was chosen next by the single player track selection logic
if(m_PlaySequentialMusicTracks)
{
audRadioStationHistory *history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC));
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC);
if(naVerifyf(history && trackLists, "Failed to get history or track lists for category RADIO_TRACK_CAT_MUSIC"))
{
const s32 numTracks = trackLists->ComputeNumTracks();
(*history)[0] = numTracks > 0 ? ((index + 1) % numTracks) : 0;
history->SetWriteIndex(1);
}
}
wasNetworkSelectedTrack = true;
}
else
{
category = ComputeNextTrackCategory(timeInMs);
trackMetadata = ComputeRandomTrack(category, index);
//#if RSG_PC
// // this would be used for sequential non random user music
// if(m_IsUserStation && /*isSequentialMode*/)
// {
// index = audRadioStation::GetUserRadioTrackManagaer()->GetNextTrack();
// }
//#endif
}
if(!trackMetadata)
{
// this can happen if all of the tracks in the chosen category are already in history, which is more likely for weather/news etc
// fall back to music in this case
#if RSG_PC
if(m_IsUserStation)
{
RADIODEBUG2("No valid tracks for %s, falling back to adverts", TrackCats_ToString((TrackCats)category));
category = RADIO_TRACK_CAT_ADVERTS;
trackMetadata = ComputeRandomTrack(category, index);
}
else
#endif // RSG_PC
{
RADIODEBUG2("No valid tracks for %s, falling back to music", TrackCats_ToString((TrackCats)category));
category = RADIO_TRACK_CAT_MUSIC;
trackMetadata = ComputeRandomTrack(category, index);
}
}
// Valid to have no tracks on the USB station until something is unlocked
if (!IsUSBMixStation())
{
audAssertf(trackMetadata, "%s failed to compute random track from RADIO_TRACK_CAT_MUSIC category", GetName());
}
if(trackMetadata)
{
// randomise start offset if this is the first track we're playing
nextTrack.Init(category, index, trackMetadata->SoundRef, m_IsFirstTrack ? m_FirstTrackStartOffset : 0.f, m_IsMixStation && !IsUSBMixStation(), m_IsReverbStation, m_NameHash);
audAssertf(nextTrack.GetDuration() > 0, "Invalid track/sound metadata: %s (%s : %u)", GetName(), TrackCats_ToString((TrackCats)category), index);
m_IsFirstTrack = false;
LogTrack(category, 0, index, trackMetadata->SoundRef);
// We don't want to do the takeover track selection if this was a remote selected track as all of our random
// seed etc. info is being extracted after this process has already been done
if (!wasNetworkSelectedTrack)
{
if (category == RADIO_TRACK_CAT_TAKEOVER_IDENTS)
{
m_TakeoverLastTimeMs = m_StationAccumulatedPlayTimeMs;
Assign(m_TakeoverMusicTrackCounter, GetRandomNumberInRange(m_TakeoverMinTracks, m_TakeoverMaxTracks));
RADIODEBUG2("Started Takeover Segment - %d tracks", m_TakeoverMusicTrackCounter);
}
if (category == RADIO_TRACK_CAT_TAKEOVER_MUSIC)
{
// continue to update the LastTakeoverTimer while we're in the takeover segment to avoid coming
// back too quickly
m_TakeoverLastTimeMs = m_StationAccumulatedPlayTimeMs;
if (m_TakeoverMusicTrackCounter > 0)
{
m_TakeoverMusicTrackCounter--;
}
}
if(m_HasTakeoverContent)
{
if(category == RADIO_TRACK_CAT_MUSIC)
{
m_MusicRunningCount++;
}
else
{
m_MusicRunningCount = 0;
}
}
m_StationAccumulatedPlayTimeMs += nextTrack.GetDuration();
}
//Log the next time that this track category can be selected.
//NOTE: We log a track category when it is selected, rather than heard, to ensure that we maintain a reasonable
//distribution of valid selection times across virtualised stations.
const RadioTrackCategoryData *categoryData = sm_MinTimeBetweenRepeatedCategories;
#if RSG_PC
if(m_IsUserStation && sm_MinTimeBetweenRepeatedCategoriesUser)
{
categoryData = sm_MinTimeBetweenRepeatedCategoriesUser;
}
#endif
SetNextValidSelectionTime(category, timeInMs + categoryData->Category[category].Value);
return true;
}
}
return false;
}
void audRadioStation::SetNextValidSelectionTime(const s32 category, const u32 timeMs)
{
m_NextValidSelectionTime[category] = timeMs;
}
void audRadioStation::UnlockMissionNewsStory(const u32 newsStory)
{
const u32 storyId = newsStory - 1;
if(naVerifyf(storyId < kMaxNewsStories, "Story id %d is out of bounds", storyId))
{
sm_NewsStoryState[storyId] = (RADIO_NEWS_UNLOCKED | RADIO_NEWS_ONCE_ONLY);
naDisplayf("Unlocked mission news story %u", newsStory);
}
// check that we don't have too many news stories unlocked; lock old stories if we do
u32 numUnlocked = 0;
for(u32 i = 0; i < kMaxNewsStories; i++)
{
if(sm_NewsStoryState[i] == (RADIO_NEWS_ONCE_ONLY|RADIO_NEWS_UNLOCKED))
{
numUnlocked++;
}
}
// only allow two unlocked stories
if(numUnlocked > 2)
{
numUnlocked -= 2;
for(u32 i = 0; i < kMaxNewsStories && numUnlocked > 0; i++)
{
if(sm_NewsStoryState[i] == (RADIO_NEWS_ONCE_ONLY|RADIO_NEWS_UNLOCKED))
{
sm_NewsStoryState[i] &= ~(RADIO_NEWS_UNLOCKED);
naDisplayf("Too many news stories unlocked; locking %u", i+1);
numUnlocked--;
}
}
}
}
void audRadioStation::ForceNextTrack(const char* categoryName, const u32 contextHash, const u32 trackNameHash, bool ignoreLocking)
{
TrackCats category = TrackCats_Parse(categoryName, TRACKCATS_MAX);
if (category != TRACKCATS_MAX)
{
const audRadioStationTrackListCollection* trackListCollection = FindTrackListsForCategory(category);
if (trackListCollection)
{
s32 numTracks = trackListCollection->ComputeNumTracks(kNullContext, ignoreLocking);
for (u32 i = 0; i < numTracks; i++)
{
if (trackListCollection->GetTrack(i, kNullContext, ignoreLocking)->SoundRef == trackNameHash)
{
ForceNextTrack((u8)category, contextHash, (u16)i);
return;
}
}
}
}
}
void audRadioStation::ForceNextTrack(const u8 category, const u32 context, const u16 index)
{
m_ForceNextTrackCategory = category;
m_ForceNextTrackContext = context;
m_ForceNextTrackIndex = index;
const u8 nextTrackIndex = (m_ActiveTrackIndex + 1) % 2;
audDisplayf("%s is forcing next track to category %u context %u track %u", GetName(), category, context, index);
if(m_Tracks[nextTrackIndex].IsInitialised())
{
m_Tracks[nextTrackIndex].Shutdown();
m_IsPlayingOverlappedTrack = false;
m_IsRequestingOverlappedTrackPrepare = false;
}
}
struct trackEntry
{
trackEntry(){}
trackEntry(u8 i, u8 o) : index(i), order(o) { }
u8 index;
u8 order;
};
int TrackEntryCompare(const trackEntry *l, const trackEntry *r)
{
return (int)l->order - (int)r->order;
}
void audRadioStation::QueueTrackList(const RadioStationTrackList *trackList, const bool forceNow)
{
if(trackList->numTracks == 0)
{
NOTFINAL_ONLY(audWarningf("%s - Ignoring empty track list %s", GetName(), audNorthAudioEngine::GetMetadataManager().GetNameFromNTO_Debug(trackList->NameTableOffset));)
return;
}
if(m_QueuedTrackList)
{
NOTFINAL_ONLY(audWarningf("%s - Replacing queued list %s (at track index %u) with %s", GetName(), audNorthAudioEngine::GetMetadataManager().GetNameFromNTO_Debug(m_QueuedTrackList->NameTableOffset), m_QueuedTrackListIndex, audNorthAudioEngine::GetMetadataManager().GetNameFromNTO_Debug(trackList->NameTableOffset));)
}
m_QueuedTrackListIndex = 0;
m_QueuedTrackList = trackList;
if(AUD_GET_TRISTATE_VALUE(trackList->Flags, FLAG_ID_RADIOSTATIONTRACKLIST_SHUFFLE) == AUD_TRISTATE_TRUE)
{
atFixedArray<trackEntry, RadioStationTrackList::MAX_TRACKS> shuffler;
for(u32 i = 0; i < trackList->numTracks; i++)
{
if(m_MusicTrackHistory.IsInHistory(trackList->Track[i].SoundRef, 4))
{
// push recently heard tracks to the back of the queue
shuffler.Push(trackEntry((u8)i, 255));
}
else
{
shuffler.Push(trackEntry((u8)i, (u8)audEngineUtil::GetRandomInteger()));
}
}
shuffler.QSort(0, -1, &TrackEntryCompare);
atRangeArray<RadioStationTrackList::tTrack, RadioStationTrackList::MAX_TRACKS> tracks;
sysMemCpy(&tracks, &trackList->Track[0], sizeof(RadioStationTrackList::tTrack) * trackList->numTracks);
for(u32 i = 0; i < trackList->numTracks; i++)
{
const_cast<RadioStationTrackList*>(trackList)->Track[i] = tracks[shuffler[i].index];
}
}
audRadioTrack &activeTrack = m_Tracks[m_ActiveTrackIndex];
const u32 nextTrackIndex = (m_ActiveTrackIndex + 1) &1;
audRadioTrack &nextTrack = m_Tracks[nextTrackIndex];
if(forceNow)
{
if(nextTrack.IsInitialised())
{
nextTrack.Shutdown();
}
if(activeTrack.IsInitialised())
{
activeTrack.Shutdown();
}
}
else
{
// If there's time, cancel the next track and choose again
if(nextTrack.IsInitialised() && activeTrack.IsInitialised() && activeTrack.GetDuration() - (u32)activeTrack.GetPlayTime() > 45000U)
{
nextTrack.Shutdown();
}
}
NOTFINAL_ONLY(audDisplayf("%s - Queued track list %s", GetName(), audNorthAudioEngine::GetMetadataManager().GetNameFromNTO_Debug(m_QueuedTrackList->NameTableOffset));)
}
void audRadioStation::ClearQueuedTracks()
{
if(m_QueuedTrackList)
{
NOTFINAL_ONLY(audDisplayf("%s - Cleared queued track list %s", GetName(), audNorthAudioEngine::GetMetadataManager().GetNameFromNTO_Debug(m_QueuedTrackList->NameTableOffset));)
m_QueuedTrackList = NULL;
m_QueuedTrackListIndex = 0;
audRadioTrack &activeTrack = m_Tracks[m_ActiveTrackIndex];
const u32 nextTrackIndex = (m_ActiveTrackIndex + 1) &1;
audRadioTrack &nextTrack = m_Tracks[nextTrackIndex];
// If there's time, cancel the next track and choose again
if(nextTrack.IsInitialised() && activeTrack.IsInitialised() && activeTrack.GetDuration() - (u32)activeTrack.GetPlayTime() > 45000U)
{
nextTrack.Shutdown();
}
}
}
bool audRadioStation::ShouldOnlyPlayMusic() const
{
#if RSG_PC
if(m_IsUserStation)
{
return (sm_UserRadioTrackManager.GetRadioMode() != USERRADIO_PLAY_MODE_RADIO);
}
#endif
if(IsNetworkModeHistoryActive() || PARAM_disableradiohistory.Get())
return false; // don't allow switch to music-only as it will break the determinism that we rely on for network sync
return (!m_ShouldPlayFullRadio || m_ScriptSetMusicOnly);
}
void audRadioStation::DumpNewsState()
{
u32 numNewsStoriesUnlocked = 0;
u32 genericNewsStories = 0;
for(u32 i = 0; i < kMaxNewsStories; i++)
{
if(sm_NewsStoryState[i] & RADIO_NEWS_UNLOCKED)
{
numNewsStoriesUnlocked++;
if(sm_NewsStoryState[i] & RADIO_NEWS_ONCE_ONLY)
{
naDisplayf("Mission news %u unlocked", i);
}
else
{
genericNewsStories++;
}
}
}
naDisplayf("%u unlocked news stories (%u generic)", numNewsStoriesUnlocked, genericNewsStories);
}
u32 audRadioStation::GetNextValidSelectionTime(const s32 category) const
{
bool disableNetworkModeForStation = false;
WIN32PC_ONLY(disableNetworkModeForStation = m_IsUserStation);
if(!disableNetworkModeForStation && (IsNetworkModeHistoryActive() || PARAM_disableradiohistory.Get()))
return 0U; // always valid
return m_NextValidSelectionTime[category];
}
bool audRadioStation::LockTrackList(const char *trackListName)
{
const RadioStationTrackList *trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(trackListName);
return LockTrackList(trackList);
}
bool audRadioStation::LockTrackList(u32 trackListName)
{
const RadioStationTrackList *trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(trackListName);
return LockTrackList(trackList);
}
bool audRadioStation::LockTrackList(const RadioStationTrackList *trackList)
{
if (trackList)
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(trackList->Category));
if (audVerifyf(trackListCollection, "%s - Failed to find track lists for %s", GetName(), TrackCats_ToString((TrackCats)trackList->Category)))
{
const bool ret = trackListCollection->LockTrackList(trackList);
if (ret)
{
audDisplayf("Locked track list %s on %s", audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackList->NameTableOffset), GetName());
if(m_UseRandomizedStrideSelection)
{
RandomizeCategoryStride((TrackCats)trackList->Category, false, true);
}
}
return ret;
}
}
return false;
}
bool audRadioStation::IsTrackListUnlocked(const char *trackListName) const
{
const RadioStationTrackList *trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(trackListName);
return IsTrackListUnlocked(trackList);
}
bool audRadioStation::IsTrackListUnlocked(u32 trackListName) const
{
const RadioStationTrackList *trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(trackListName);
return IsTrackListUnlocked(trackList);
}
bool audRadioStation::IsTrackListUnlocked(const RadioStationTrackList *trackList) const
{
if(trackList)
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(trackList->Category));
if(audVerifyf(trackListCollection, "%s - Failed to find track lists for %s", GetName(), TrackCats_ToString((TrackCats)trackList->Category)))
{
return trackListCollection->IsTrackListUnlocked(trackList);
}
}
return false;
}
bool audRadioStation::UnlockTrackList(const char *trackListName)
{
const RadioStationTrackList *trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(trackListName);
return UnlockTrackList(trackList);
}
bool audRadioStation::UnlockTrackList(u32 trackListName)
{
const RadioStationTrackList *trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(trackListName);
return UnlockTrackList(trackList);
}
bool audRadioStation::UnlockTrackList(const RadioStationTrackList *trackList)
{
if(trackList)
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(trackList->Category));
if(audVerifyf(trackListCollection, "%s - Failed to find track lists for %s", GetName(), TrackCats_ToString((TrackCats)trackList->Category)))
{
const bool ret = trackListCollection->UnlockTrackList(trackList);
if(ret)
{
audDisplayf("Unlocked track list %s on %s", audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackList->NameTableOffset), GetName());
if(m_UseRandomizedStrideSelection)
{
RandomizeCategoryStride((TrackCats)trackList->Category, false, true);
}
}
return ret;
}
}
return false;
}
// Force the given music track list to be the only available track list (used for USB station behavior)
bool audRadioStation::ForceMusicTrackList(const u32 trackListName, u32 timeOffsetMs)
{
Freeze();
RadioStationTrackList* trackList = audNorthAudioEngine::GetMetadataManager().GetObject<RadioStationTrackList>(trackListName);
if (trackList)
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
for (u32 i = 0; i < trackListCollection->GetListCount(); i++)
{
if (trackListCollection->GetList(i) == trackList)
{
trackListCollection->UnlockTrackList(trackListCollection->GetList(i));
}
else
{
trackListCollection->LockTrackList(trackListCollection->GetList(i));
}
}
SkipToOffsetInMusicTrackList(timeOffsetMs, true);
}
m_LastForcedMusicTrackList = trackListName;
}
Unfreeze();
return true;
}
// Skip to the given offset relative to the start of the currently active tracklist (used for USB station behavior)
void audRadioStation::SkipToOffsetInMusicTrackList(u32 timeOffsetMs, bool allowWrap)
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
const RadioStationTrackList::tTrack* selectedTrack = GetMusicTracklistTrackForTimeOffset(timeOffsetMs, allowWrap);
if (selectedTrack)
{
u32 totalTracksIncludingLocked = trackListCollection->ComputeNumTracks(0u, true);
for (u32 i = 0; i < totalTracksIncludingLocked; i++)
{
if (trackListCollection->GetTrack(i, 0u, true) == selectedTrack)
{
audRadioStationHistory* history = const_cast<audRadioStationHistory*>(FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC));
if (history)
{
(*history)[0] = i;
const s32 trackIndex = GetMusicTrackIndexInTrackList(selectedTrack);
const s32 trackStartTime = GetMusicTrackListTrackStartTimeMs(trackIndex);
const s32 trackDuration = GetMusicTrackListTrackDurationMs(trackIndex);
m_FirstTrackStartOffset = Clamp((timeOffsetMs - trackStartTime) / (f32)trackDuration, 0.f, 1.f);
m_IsFirstTrack = true;
m_ActiveTrackIndex = 0;
m_Tracks[0].Shutdown();
m_Tracks[1].Shutdown();
PrepareNextTrack();
}
break;
}
}
}
}
}
// Query the current time offset into the current music track list (used for USB station behavior)
u32 audRadioStation::GetMusicTrackListCurrentPlayTimeMs() const
{
u32 startTime = 0u;
s32 playingTrackIndex = GetMusicTrackListActiveTrackIndex();
if (playingTrackIndex >= 0)
{
startTime = GetMusicTrackListTrackStartTimeMs((u32)playingTrackIndex);
}
return startTime + m_Tracks[m_ActiveTrackIndex].GetPlayTime();
}
// Get the track that will be playing at the given time offset
const RadioStationTrackList::tTrack* audRadioStation::GetMusicTracklistTrackForTimeOffset(u32 timeOffsetMs, bool allowWrap) const
{
audRadioStationTrackListCollection* trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
u32 totalDuration = 0u;
const u32 numUnlockedTracks = trackListCollection->ComputeNumTracks();
if(allowWrap)
{
for (u32 i = 0; i < numUnlockedTracks; i++)
{
if (const RadioStationTrackList::tTrack* track = trackListCollection->GetTrack(i))
{
const StreamingSound *streamingSound = SOUNDFACTORY.DecompressMetadata<StreamingSound>(track->SoundRef);
totalDuration += streamingSound ? streamingSound->Duration : 0u;
}
}
timeOffsetMs %= totalDuration;
}
u32 cumulativeDuration = 0;
for (u32 i = 0; i < numUnlockedTracks; i++)
{
const RadioStationTrackList::tTrack* track = trackListCollection->GetTrack(i);
if (track)
{
const StreamingSound *streamingSound = SOUNDFACTORY.DecompressMetadata<StreamingSound>(track->SoundRef);
cumulativeDuration += streamingSound ? streamingSound->Duration : 0u;
if (timeOffsetMs < cumulativeDuration)
{
return track;
}
}
}
}
return nullptr;
}
// Get the track that will be playing at the given index offset
const RadioStationTrackList::tTrack* audRadioStation::GetMusicTrackListTrackForIndex(u32 trackIndex) const
{
audRadioStationTrackListCollection* trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
if (audVerifyf(trackIndex < trackListCollection->ComputeNumTracks(), "Track %u exceeds total number of tracks (%u)", trackIndex, trackListCollection->ComputeNumTracks()))
{
return trackListCollection->GetTrack(trackIndex);
}
}
return nullptr;
}
// Get the track ID for the track playing at the givne offset in the currently active music playlist (used for USB station behavior)
u32 audRadioStation::GetMusicTrackListTrackIDForTimeOffset(u32 timeOffsetMs) const
{
const RadioStationTrackList::tTrack* track = GetMusicTracklistTrackForTimeOffset(timeOffsetMs);
return track ? audRadioTrack::FindTextIdForSoundHash(track->SoundRef) : 0u;
}
// Get the track ID for the given track in the currently active music playlist (used for USB station behavior)
u32 audRadioStation::GetMusicTrackListTrackIDForIndex(u32 trackIndex) const
{
const RadioStationTrackList::tTrack* track = GetMusicTrackListTrackForIndex(trackIndex);
return track ? audRadioTrack::FindTextIdForSoundHash(track->SoundRef) : 0u;
}
// Query the total number of tracks in the current track list (used for USB station behavior)
u32 audRadioStation::GetMusicTrackListNumTracks() const
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
return trackListCollection ? trackListCollection->ComputeNumTracks() : 0;
}
// Query the total duration of the current music track list (used for USB station behavior)
u32 audRadioStation::GetMusicTrackListDurationMs() const
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
return trackListCollection ? GetMusicTrackListTrackStartTimeMs(trackListCollection->ComputeNumTracks()) : 0;
}
// Query the duration of the music track at the given index in the active music track list (used for USB station behavior)
u32 audRadioStation::GetMusicTrackListTrackDurationMs(u32 trackIndex) const
{
const RadioStationTrackList::tTrack* track = GetMusicTrackListTrackForIndex(trackIndex);
const StreamingSound *streamingSound = track ? SOUNDFACTORY.DecompressMetadata<StreamingSound>(track->SoundRef) : nullptr;
return streamingSound ? streamingSound->Duration : 0u;
}
// Get the start time of the given track in the music track list (used for USB station behavior)
s32 audRadioStation::GetMusicTrackListTrackStartTimeMs(u32 trackIndex) const
{
u32 cumulativeDuration = 0;
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
const u32 numUnlockedTracks = trackListCollection->ComputeNumTracks();
for (u32 i = 0; i < trackIndex && i < numUnlockedTracks; i++)
{
const StreamingSound *streamingSound = SOUNDFACTORY.DecompressMetadata<StreamingSound>(trackListCollection->GetTrack(i)->SoundRef);
cumulativeDuration += streamingSound ? streamingSound->Duration : 0u;
}
}
return cumulativeDuration;
}
// Get the index of the given track in the playing tracklist (used for USB station behavior)
s32 audRadioStation::GetMusicTrackIndexInTrackList(const RadioStationTrackList::tTrack* track) const
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
const u32 numUnlockedTracks = trackListCollection->ComputeNumTracks();
for (u32 i = 0; i < numUnlockedTracks; i++)
{
if (trackListCollection->GetTrack(i) == track)
{
return i;
}
}
}
return -1;
}
// Get the track index of the currently playing track relative to the music track list (used for USB station behavior)
s32 audRadioStation::GetMusicTrackListActiveTrackIndex() const
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
const s32 numUnlockedTracks = trackListCollection->ComputeNumTracks();
for (s32 i = 0; i < numUnlockedTracks; i++)
{
const RadioStationTrackList::tTrack* track = trackListCollection->GetTrack(i);
if (track && track->SoundRef == m_Tracks[m_ActiveTrackIndex].GetSoundRef())
{
return i;
}
}
}
return -1;
}
u32 audRadioStation::GetPlayingTrackListNextTrackTimeOffset()
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
const s32 nextTrackIndex = GetMusicTrackListActiveTrackIndex() + 1;
const u32 numUnlockedTracks = trackListCollection->ComputeNumTracks();
if (nextTrackIndex < numUnlockedTracks)
{
return GetMusicTrackListTrackStartTimeMs(nextTrackIndex);
}
}
// Wrap around (needs confirming if this is desired behavior)
return 0;
}
u32 audRadioStation::GetPlayingTrackListPrevTrackTimeOffset()
{
audRadioStationTrackListCollection *trackListCollection = const_cast<audRadioStationTrackListCollection *>(FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC));
if (trackListCollection)
{
const s32 prevTrackIndex = GetMusicTrackListActiveTrackIndex() - 1;
const u32 numUnlockedTracks = trackListCollection->ComputeNumTracks();
if (prevTrackIndex >= 0)
{
return GetMusicTrackListTrackStartTimeMs((u32)prevTrackIndex);
}
// Wrap around (needs confirming if this is desired behavior)
return GetMusicTrackListTrackStartTimeMs(numUnlockedTracks - 1);
}
return 0u;
}
bool audRadioStation::IsCurrentTrackNew() const
{
const audRadioTrack &track = GetCurrentTrack();
const audRadioStationTrackListCollection * trackLists = FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC);
if(trackLists &&
track.GetCategory() == RADIO_TRACK_CAT_MUSIC &&
track.GetTrackIndex() >= trackLists->GetList(0)->numTracks)
{
return true;
}
return false;
}
#if __BANK
u16 g_ForceNextTrackIndex = 0xffff;
u8 g_ForceNextTrackCategory = 0;
char g_ForceStationName[128]={0};
char g_ForceTrackContextName[128]={0};
char g_ForceTrackSettings[128]={0};
char g_TrackListName[128]={0};
u32 g_DebugNewsStory = 0;
void UnlockNewsCB()
{
audRadioStation::UnlockMissionNewsStory(g_DebugNewsStory);
}
void Debug_ForceNextTrackCB()
{
audRadioStation *station = audRadioStation::FindStation(g_ForceStationName);
if(station)
{
station->ForceNextTrack(g_ForceNextTrackCategory, atStringHash(g_ForceTrackContextName), g_ForceNextTrackIndex);
}
}
void ForceNextTrackCB()
{
audRadioStation *station = audRadioStation::FindStation(g_ForceStationName);
if(station)
{
station->ForceTrack(g_ForceTrackSettings);
}
}
void DumpNewsStateCB()
{
audRadioStation::DumpNewsState();
}
bool g_ForceQueueTrack= true;
void QueueTrackListOnStationCB()
{
audRadioStation *station = audRadioStation::FindStation(g_ForceStationName);
if(station)
{
const RadioStationTrackList *trackList = audNorthAudioEngine::GetObject<RadioStationTrackList>(g_TrackListName);
if(trackList)
{
station->QueueTrackList(trackList, g_ForceQueueTrack);
}
else
{
audWarningf("Failed to find track list %s", audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackList->NameTableOffset));
}
}
}
void ClearQueuedTracksOnStationCB()
{
audRadioStation *station = audRadioStation::FindStation(g_ForceStationName);
if(station)
{
station->ClearQueuedTracks();
}
}
void SnapshotStationCB()
{
audRadioStation::GetStation(g_TestSyncStationIndex)->PopulateSyncData(g_TestSyncData);
}
void ApplyStationSnapshotCB()
{
audRadioStation::GetStation(g_TestSyncStationIndex)->SyncStation(g_TestSyncData);
}
void UnlockTrackListCB()
{
audRadioStation *station = audRadioStation::FindStation(g_ForceStationName);
if(station)
{
station->UnlockTrackList(g_ForceTrackSettings);
}
}
void LockTrackListCB()
{
audRadioStation *station = audRadioStation::FindStation(g_ForceStationName);
if(station)
{
station->LockTrackList(g_ForceTrackSettings);
}
}
void SwitchUSBStationMixCB()
{
audRadioStation* station = audRadioStation::GetUSBMixStation();
if (station)
{
const char* trackListName = g_RequestedUSBTrackListComboIndex >= 0 ? g_USBTrackListNames[g_RequestedUSBTrackListComboIndex] : nullptr;
station->ForceMusicTrackList(atStringHash(trackListName), g_USBStationTrackListOffsetMs);
station->SetLocked(false);
}
}
void USBStationSkipToOffsetCB()
{
audRadioStation* station = audRadioStation::GetUSBMixStation();
if (station)
{
station->SkipToOffsetInMusicTrackList(g_USBStationTrackListOffsetMs);
}
}
void USBStationSkipPrevTrackCB()
{
audRadioStation* station = audRadioStation::GetUSBMixStation();
if (station)
{
s32 activeTrack = station->GetMusicTrackListActiveTrackIndex();
if (activeTrack >= 0)
{
s32 prevTrackStartTime = station->GetMusicTrackListTrackStartTimeMs(activeTrack - 1);
station->SkipToOffsetInMusicTrackList(prevTrackStartTime);
}
}
}
void USBStationSkipNextTrackCB()
{
audRadioStation* station = audRadioStation::GetUSBMixStation();
if (station)
{
s32 activeTrack = station->GetMusicTrackListActiveTrackIndex();
if (activeTrack >= 0)
{
s32 nextTrackStartTime = station->GetMusicTrackListTrackStartTimeMs(activeTrack + 1);
station->SkipToOffsetInMusicTrackList(nextTrackStartTime);
}
}
}
void USBStationScriptLockTrackListCB()
{
if (audRadioStation* station = audRadioStation::GetUSBMixStation())
{
const char* trackListName = g_RequestedUSBTrackListComboIndex >= 0 ? g_USBTrackListNames[g_RequestedUSBTrackListComboIndex] : nullptr;
u32 trackListNameHash = atStringHash(trackListName);
// This duplicates what the script command does
if(trackListNameHash != ATSTRINGHASH("TUNER_AP_SILENCE_MUSIC", 0x68865F9) && trackListNameHash == station->GetLastForcedMusicTrackList())
{
station->ForceMusicTrackList(ATSTRINGHASH("TUNER_AP_SILENCE_MUSIC", 0x68865F9), 0u);
}
}
}
void DumpplayerStationtracksCB()
{
if(const audRadioStation *station=g_RadioAudioEntity.GetPlayerRadioStation())
{
station->DumpTrackList();
}
}
void audRadioStation::DumpTrackList() const
{
char textIdName[32];
const audRadioStationTrackListCollection *trackLists = FindTrackListsForCategory(RADIO_TRACK_CAT_MUSIC);
for(s32 listIdx = 0; listIdx < trackLists->GetListCount(); listIdx++)
{
const RadioStationTrackList *trackList = trackLists->GetList(listIdx);
const char *trackListName = audNorthAudioEngine::GetMetadataManager().GetObjectNameFromNameTableOffset(trackList->NameTableOffset);
for(s32 i = 0; i < trackList->numTracks; i++)
{
const u32 soundNameHash = trackList->Track[i].SoundRef;
formatf(textIdName, "RTT_%08X", soundNameHash);
const RadioTrackTextIds *rtt = audNorthAudioEngine::GetMetadataManager().GetObject<RadioTrackTextIds>(textIdName);
const char *soundName = g_AudioEngine.GetSoundManager().GetFactory().GetMetadataManager().GetObjectName(soundNameHash);
if(rtt)
{
if(rtt->numTextIds != 1)
{
audErrorf("%s - %s - %s - %d - Radio track with %u text Ids", GetName(), trackListName, soundName, i, rtt->numTextIds);
}
const u32 textId = rtt->TextId[0].TextId;
char artistID[16];
char trackID[16];
formatf(artistID,"%dA", textId);
formatf(trackID,"%dS", textId);
const char *artistName = TheText.Get(artistID);
const char *trackTitle = TheText.Get(trackID);
audDisplayf("%s,%s,%d,%u,%s,\"%s\",\"%s\",%u", GetName(), trackListName, i, soundNameHash,soundName, artistName, trackTitle, textId);
}
else
{
audDisplayf("%s,%s,%d,%u,%s,%s,%s,%u", GetName(), trackListName, i, soundNameHash, soundName, "MISSING", "MISSING", 0);
}
}
}
}
void audRadioStation::AddWidgets(bkBank &bank)
{
bank.AddButton("DumpPlayerStationTracks", CFA(DumpplayerStationtracksCB));
bank.AddToggle("DisableOverlappedTracks", &sm_DisableOverlap);
bank.AddToggle("Disable Play Time Compensation", &g_DisablePlayTimeCompensation);
bank.AddToggle("DebugHistory", &sm_DebugHistory);
bank.AddButton("Snapshot", CFA(SnapshotStationCB));
bank.AddButton("Apply", CFA(ApplyStationSnapshotCB));
bank.AddSlider("Snapshot/Apply Station Index", &g_TestSyncStationIndex, 0, 30, 1);
bank.AddToggle("DebugRadioRandom", &g_DebugRadioRandom);
bank.PushGroup("Radio Wheel Winding");
bank.AddSlider("Station Wind Start", &g_RadioWheelWindStart, 0, 100000, 100);
bank.AddSlider("Station Wind Offset", &g_RadioWheelWindOffset, 0, 100000, 100);
bank.AddButton("Re-Sort Stations", CFA(SortRadioStationList));
bank.PopGroup();
bank.PushGroup("Transitions");
bank.AddSlider("Mix Transition Time", &g_RadioTrackMixtransitionTimeMs, 0, 60000, 100);
bank.AddSlider("Mix Transition Offset Time", &g_RadioTrackMixtransitionOffsetTimeMs, 0, 60000, 100);
bank.AddSlider("Track Overlap Time", &g_RadioTrackOverlapTimeMs, 0, 60000, 100);
bank.AddSlider("User Track Overlap Time", &g_UserRadioTrackOverlapTimeMs, 0, 60000, 100);
bank.AddSlider("Track Prepare Time", &g_RadioTrackPrepareTimeMs, 0, 60000, 100);
bank.AddSlider("Track Skip End Time", &g_RadioTrackSkipEndTimeMs, 0, 60000, 100);
bank.AddToggle("Use Precise Crossfades", &g_UsePreciseTrackCrossfades);
bank.AddSlider("Precise Crossfade Play Time", &g_RadioTrackOverlapPlayTimeMs, 0, 60000, 100);
bank.PopGroup();
bank.PushGroup("ForceNextTrack");
bank.AddText("Station", &g_ForceStationName[0], sizeof(g_ForceStationName));
bank.AddText("Track", &g_ForceTrackSettings[0], sizeof(g_ForceTrackSettings));
bank.AddButton("ForceTrack", CFA(ForceNextTrackCB));
bank.AddButton("UnlockTrackList", CFA(UnlockTrackListCB));
bank.AddButton("LockTrackList", CFA(LockTrackListCB));
bank.AddSlider("Debug_ForceIndex", &g_ForceNextTrackIndex, 0, 0xffff, 1);
bank.AddSlider("Debug_ForceCategory", &g_ForceNextTrackCategory, 0, 255, 1);
bank.AddText("Debug_ForceContext", &g_ForceTrackContextName[0], sizeof(g_ForceTrackContextName));
bank.AddButton("Debug_ForceNextTrack", CFA(Debug_ForceNextTrackCB));
bank.AddToggle("ForceDjSpeech", &g_ForceDjSpeech);
bank.AddText("TrackListName", &g_TrackListName[0], sizeof(g_TrackListName));
bank.AddButton("QueueListOnStation", CFA(QueueTrackListOnStationCB));
bank.AddToggle("ForceQueuedTrack", &g_ForceQueueTrack);
bank.AddButton("ClearQueuedTracksOnStation", CFA(ClearQueuedTracksOnStationCB));
bank.AddSlider("NewsStory", &g_DebugNewsStory, 0, kMaxNewsStories, 1);
bank.AddButton("UnlockNews", CFA(UnlockNewsCB));
bank.PopGroup();
g_USBStationBank = &bank;
g_USBStationGroup = bank.PushGroup("USB Station");
bank.AddButton("Add USB Station Widgets", CFA(AddUSBStationWidgets));
bank.PopGroup();
bank.PushGroup("Radio DJ", false);
bank.AddSlider("DJ ducking level (dB)", &g_RadioDjDuckingLevelDb, 0.f, 24.f, 0.5f);
bank.AddSlider("DJ ducking smooth time (ms)", &g_RadioDjDuckTimeMs, 0, 1000, 100);
bank.AddSlider("DJ Speech Probability", &g_RadioDjSpeechProbability, 0.f, 1.f, 0.1f);
bank.AddSlider("DJ Speech Probability (Kult)", &g_RadioDjSpeechProbabilityKult, 0.f, 1.f, 0.1f);
bank.AddSlider("DJ Speech Probability (Motomami Outro)", &g_RadioDjOutroSpeechProbabilityMotomami, 0.f, 1.f, 0.1f);
bank.AddSlider("DJ Speech Min Retrigger Time Ms (Motomami)", &g_RadioDjSpeechMinDelayMsMotomami, 0u, 100000u, 100u);
bank.AddSlider("DJ Time Probability", &g_RadioDjTimeProbability, 0.f, 1.f, 0.1f);
bank.PopGroup();
bank.AddButton("DumpNewsState", CFA(DumpNewsStateCB));
bank.AddToggle("LogRadio", &g_LogRadio);
}
void audRadioStation::AddUSBStationWidgets()
{
if (g_USBStationBank && g_USBStationGroup && g_NumUSBStationTrackLists > 0 && !g_HasAddedUSBStationWidgets)
{
bkBank& bank = *g_USBStationBank;
bank.SetCurrentGroup(*g_USBStationGroup);
bank.AddToggle("Debug Draw USB Station", &g_DebugDrawUSBStation);
g_USBStationTrackListCombo = bank.AddCombo("Track List", &g_RequestedUSBTrackListComboIndex, g_NumUSBStationTrackLists, g_USBTrackListNames.GetElements());
bank.AddSlider("Start Offset (ms)", &g_USBStationTrackListOffsetMs, 0, 3600000, 1000);
bank.AddButton("Set Track List", CFA(SwitchUSBStationMixCB));
bank.AddButton("Skip To Offset", CFA(USBStationSkipToOffsetCB));
bank.AddButton("<< Skip Track", CFA(USBStationSkipPrevTrackCB));
bank.AddButton("Skip Track >>", CFA(USBStationSkipNextTrackCB));
bank.AddButton("Lock Track List", CFA(USBStationScriptLockTrackListCB));
g_HasAddedUSBStationWidgets = true;
}
}
void audRadioStation::FormatTimeString(const u32 milliseconds, char *destBuffer, const u32 destLength)
{
float seconds = milliseconds * 0.001f;
u32 minutes = (u32)(seconds / 60.f);
f32 remainderSeconds = seconds - (minutes*60.f);
formatf(destBuffer, destLength, "%02u:%02.02f", minutes, remainderSeconds);
}
void audRadioStation::DrawUSBStation() const
{
const f32 yStepRate = 0.015f;
f32 yCoord = 0.05f;
f32 xCoord = 0.05f;
u32 numTracks = GetMusicTrackListNumTracks();
s32 playingTrack = Max(0, GetMusicTrackListActiveTrackIndex());
char tempString[256];
formatf(tempString, "Station %s", GetName());
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Last Forced Track List: %s", audNorthAudioEngine::GetMetadataManager().GetObjectName(m_LastForcedMusicTrackList));
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Playing Track %u of %u", playingTrack, numTracks);
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Tracklist Play Time: %u/%u", GetMusicTrackListCurrentPlayTimeMs(), GetMusicTrackListDurationMs());
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
for (u32 i = 0; i < numTracks; i++)
{
s32 startTime = GetMusicTrackListTrackStartTimeMs(i);
formatf(tempString, "Track %u", i);
grcDebugDraw::Text(Vector2(xCoord, yCoord), (s32)i == playingTrack ? Color32(100, 255, 100) : Color32(255, 255, 255), tempString);
yCoord += yStepRate;
xCoord += 0.02f;
const RadioStationTrackList::tTrack* track = GetMusicTracklistTrackForTimeOffset(startTime);
if (track)
{
formatf(tempString, "Sound: %s", SOUNDFACTORY.GetMetadataManager().GetObjectName(track->SoundRef));
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Start Time: %d", startTime);
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
}
xCoord -= 0.02f;
}
yCoord += yStepRate;
}
void audRadioStation::DrawTakeoverStation() const
{
const f32 yStepRate = 0.015f;
f32 yCoord = 0.05f;
f32 xCoord = 0.05f;
char tempString[256];
formatf(tempString, "Station %s", GetName());
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Random Seed: %u", GetRandomSeed());
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Last Takeover Time: %u", m_TakeoverLastTimeMs);
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Station Accumulated Playtime: %u", m_StationAccumulatedPlayTimeMs);
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Takeover Music Track Count: %u", m_TakeoverMusicTrackCounter);
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Music Run Count: %u", m_MusicRunningCount);
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
const audRadioTrack& currentTrack = GetCurrentTrack();
if (currentTrack.IsInitialised())
{
formatf(tempString, "Current Track: %s (%s track %u)", SOUNDFACTORY.GetMetadataManager().GetObjectName(currentTrack.GetSoundRef()), TrackCats_ToString((TrackCats)currentTrack.GetCategory()), currentTrack.GetTrackIndex());
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Progress: %u/%u (%.02f%%)", currentTrack.GetPlayTime(), currentTrack.GetDuration(), (currentTrack.GetPlayTime() / (f32)currentTrack.GetDuration()) * 100.f);
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
}
else
{
formatf(tempString, "Current Track: Not Initialised");
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
}
const audRadioTrack& nextTrack = GetNextTrack();
if (nextTrack.IsInitialised())
{
formatf(tempString, "Next Track: %s (%s track %u)", SOUNDFACTORY.GetMetadataManager().GetObjectName(nextTrack.GetSoundRef()), TrackCats_ToString((TrackCats)nextTrack.GetCategory()), nextTrack.GetTrackIndex());
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
}
else
{
formatf(tempString, "Next Track: Not Initialised");
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
}
yCoord += yStepRate;
for (u32 category = 0; category < TRACKCATS_MAX; category++)
{
if (category == RADIO_TRACK_CAT_MUSIC ||
category == RADIO_TRACK_CAT_IDENTS ||
category == RADIO_TRACK_CAT_TAKEOVER_DJSOLO ||
category == RADIO_TRACK_CAT_TAKEOVER_IDENTS ||
category == RADIO_TRACK_CAT_TAKEOVER_MUSIC ||
category == RADIO_TRACK_CAT_DJSOLO)
{
s32 trackIndex = ~0u;
if (const audRadioStationHistory* history = FindHistoryForCategory(category))
{
trackIndex = history->GetPreviousEntry();
}
if (trackIndex < 0xff)
{
formatf(tempString, "Category %d: %s (%d tracks available)", category, TrackCats_ToString((TrackCats)category), ComputeNumTracksAvailable(category));
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Next Valid Selection Time: %d", GetNextValidSelectionTime(category));
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
formatf(tempString, "Category Stride: %d", m_CategoryStride[category]);
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
const RadioStationTrackList::tTrack* trackMetadata = GetTrack(category, kNullContext, trackIndex);
formatf(tempString, "Previous Track: %s (Index: %u)", trackMetadata ? SOUNDFACTORY.GetMetadataManager().GetObjectName(trackMetadata->SoundRef) : "NULL", trackIndex);
grcDebugDraw::Text(Vector2(xCoord, yCoord), Color32(255, 255, 255), tempString);
yCoord += yStepRate;
}
yCoord += yStepRate;
}
}
}
void audRadioStation::DrawDebug(audDebugDrawManager &drawMgr) const
{
if(ShouldOnlyPlayMusic())
{
drawMgr.DrawLine("MUSIC ONLY");
}
if(GetCurrentTrack().IsInitialised())
{
drawMgr.PushSection("ActiveTrack");
GetCurrentTrack().DrawDebug(drawMgr);
drawMgr.PopSection();
}
else
{
drawMgr.DrawLine("ActiveTrack NOT INITIALISED");
}
if(GetNextTrack().IsInitialised())
{
drawMgr.PushSection("NextTrack");
GetNextTrack().DrawDebug(drawMgr);
drawMgr.PopSection();
}
else
{
drawMgr.DrawLine("NextTrack NOT INITIALISED");
}
if(sm_DebugHistory && this == g_RadioAudioEntity.GetPlayerRadioStation())
{
const audRadioStationHistory *musicHistory = FindHistoryForCategory(RADIO_TRACK_CAT_MUSIC);
const s32 historyLength = Max<s32>(ComputeNumTracksAvailable(RADIO_TRACK_CAT_MUSIC) - kMinRadioTracksOutsideHistory, 0);
const s32 searchLength = Clamp<s32>(historyLength, 0, audRadioStationHistory::kRadioHistoryLength);
for(s32 i = 0; i < searchLength; i++)
{
s32 historyIndex = (musicHistory->GetWriteIndex() + audRadioStationHistory::kRadioHistoryLength - 1 - i) % audRadioStationHistory::kRadioHistoryLength;
const u32 trackId = (*musicHistory)[historyIndex];
const char *objName = g_AudioEngine.GetSoundManager().GetFactory().GetMetadataManager().GetObjectName(trackId);
#if RSG_PC
if(m_IsUserStation)
{
if(trackId < sm_UserRadioTrackManager.GetNumTracks())
objName = sm_UserRadioTrackManager.GetTrackTitle(static_cast<s32>(trackId));
else
objName = "(invalid)";
}
#endif
drawMgr.DrawLinef("%d / %d: %08X - %s", i, historyIndex, trackId, objName ? objName : "unknown");
}
}
bool isDummy = false;
if(sm_DjSpeechMonoSound != NULL)
{
u32 clientVar = 0;
sm_DjSpeechMonoSound->GetClientVariable(clientVar);
if(clientVar == 0xdeadbeef) // using special value in client variable to denote dummy DJ speech
{
isDummy = true;
}
}
const char *djStates[] = {"Idle","Preparing","Prepared","Playing"};
char djSpeechWaitTimeString[32];
FormatTimeString(sm_ActiveDjSpeechStartTimeMs > m_Tracks[m_ActiveTrackIndex].GetPlayTime() ? sm_ActiveDjSpeechStartTimeMs - m_Tracks[m_ActiveTrackIndex].GetPlayTime() : 0, djSpeechWaitTimeString);
drawMgr.DrawLinef("Dj Speech state: %s starting in %s%s", djStates[sm_DjSpeechState], djSpeechWaitTimeString, isDummy ? " DUMMY" : "");
}
const char *audRadioStation::GetTrackCategoryName(const s32 trackCategory)
{
const char *ret = TrackCats_ToString((TrackCats)trackCategory);
if(ret)
{
return ret+16;
}
return NULL;
}
#if RSG_PC
void audRadioStation::DebugDrawUserMusic()
{
//for(int i=0; i<MAX_TRACK_HISTORY; i++)
//{
// s32 trackId = sm_UserRadioTrackManager.GetTrackIdFromHistory(i);
// if(trackId != -1)
// {
// char trackName[256];
// snprintf(trackName, 128, "%s", sm_UserRadioTrackManager.GetFileName(trackId));
// //Displayf("%d : %s : %d", i, trackName, trackId);
// grcDebugDraw::PrintToScreenCoors(trackName, 10, 10+i);
// }
//}
}
#endif //WIN32PC
#endif
#endif // NA_RADIO_ENABLED