1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/audio_modem/modem_impl.h"
11 #include "base/bind.h"
12 #include "base/bind_helpers.h"
13 #include "base/command_line.h"
14 #include "base/logging.h"
15 #include "base/memory/scoped_ptr.h"
16 #include "base/run_loop.h"
17 #include "base/strings/string_util.h"
18 #include "base/strings/stringprintf.h"
19 #include "base/strings/sys_string_conversions.h"
20 #include "base/time/time.h"
21 #include "components/audio_modem/audio_modem_switches.h"
22 #include "components/audio_modem/audio_player_impl.h"
23 #include "components/audio_modem/audio_recorder_impl.h"
24 #include "components/audio_modem/public/whispernet_client.h"
25 #include "content/public/browser/browser_thread.h"
26 #include "media/audio/audio_manager.h"
27 #include "media/audio/audio_manager_base.h"
28 #include "media/base/audio_bus.h"
29 #include "third_party/webrtc/common_audio/wav_file.h"
31 namespace audio_modem
{
35 const int kMaxSamples
= 10000;
36 const int kTokenTimeoutMs
= 2000;
37 const int kMonoChannelCount
= 1;
39 // UrlSafe is defined as:
40 // '/' represented by a '_' and '+' represented by a '-'
41 // TODO(ckehoe): Move this to a central place.
42 std::string
FromUrlSafe(std::string token
) {
43 base::ReplaceChars(token
, "-", "+", &token
);
44 base::ReplaceChars(token
, "_", "/", &token
);
47 std::string
ToUrlSafe(std::string token
) {
48 base::ReplaceChars(token
, "+", "-", &token
);
49 base::ReplaceChars(token
, "/", "_", &token
);
53 // TODO(ckehoe): Move this to a central place.
54 std::string
AudioTypeToString(AudioType audio_type
) {
55 if (audio_type
== AUDIBLE
)
57 if (audio_type
== INAUDIBLE
)
60 NOTREACHED() << "Got unexpected token type " << audio_type
;
64 bool ReadBooleanFlag(const std::string
& flag
, bool default_value
) {
65 const std::string flag_value
= base::StringToLowerASCII(
66 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(flag
));
67 if (flag_value
== "true" || flag_value
== "1")
69 if (flag_value
== "false" || flag_value
== "0")
71 LOG_IF(ERROR
, !flag_value
.empty())
72 << "Unrecognized value \"" << flag_value
<< " for flag "
73 << flag
<< ". Defaulting to " << default_value
;
82 ModemImpl::ModemImpl() : client_(nullptr), recorder_(nullptr) {
83 // TODO(rkc): Move all of these into initializer lists once it is allowed.
84 should_be_playing_
[AUDIBLE
] = false;
85 should_be_playing_
[INAUDIBLE
] = false;
86 should_be_recording_
[AUDIBLE
] = false;
87 should_be_recording_
[INAUDIBLE
] = false;
89 player_enabled_
[AUDIBLE
] = ReadBooleanFlag(
90 switches::kAudioModemEnableAudibleBroadcast
, true);
91 player_enabled_
[INAUDIBLE
] = ReadBooleanFlag(
92 switches::kAudioModemEnableInaudibleBroadcast
, true);
93 player_
[AUDIBLE
] = nullptr;
94 player_
[INAUDIBLE
] = nullptr;
96 samples_caches_
.resize(2);
97 samples_caches_
[AUDIBLE
] = new SamplesMap(kMaxSamples
);
98 samples_caches_
[INAUDIBLE
] = new SamplesMap(kMaxSamples
);
101 void ModemImpl::Initialize(WhispernetClient
* client
,
102 const TokensCallback
& tokens_cb
) {
105 tokens_cb_
= tokens_cb
;
107 // These will be unregistered on destruction, so unretained is safe to use.
108 client_
->RegisterTokensCallback(
109 base::Bind(&ModemImpl::OnTokensFound
, base::Unretained(this)));
110 client_
->RegisterSamplesCallback(
111 base::Bind(&ModemImpl::OnTokenEncoded
, base::Unretained(this)));
113 if (!player_
[AUDIBLE
])
114 player_
[AUDIBLE
] = new AudioPlayerImpl();
115 player_
[AUDIBLE
]->Initialize();
117 if (!player_
[INAUDIBLE
])
118 player_
[INAUDIBLE
] = new AudioPlayerImpl();
119 player_
[INAUDIBLE
]->Initialize();
121 decode_cancelable_cb_
.Reset(base::Bind(
122 &ModemImpl::DecodeSamplesConnector
, base::Unretained(this)));
124 recorder_
= new AudioRecorderImpl();
125 recorder_
->Initialize(decode_cancelable_cb_
.callback());
127 dump_tokens_dir_
= base::FilePath(base::CommandLine::ForCurrentProcess()
128 ->GetSwitchValueNative(switches::kAudioModemDumpTokensToDir
));
131 ModemImpl::~ModemImpl() {
132 if (player_
[AUDIBLE
])
133 player_
[AUDIBLE
]->Finalize();
134 if (player_
[INAUDIBLE
])
135 player_
[INAUDIBLE
]->Finalize();
137 recorder_
->Finalize();
139 // Whispernet initialization may never have completed.
141 client_
->RegisterTokensCallback(TokensCallback());
142 client_
->RegisterSamplesCallback(SamplesCallback());
146 void ModemImpl::StartPlaying(AudioType type
) {
147 DCHECK(type
== AUDIBLE
|| type
== INAUDIBLE
);
148 should_be_playing_
[type
] = true;
149 // If we don't have our token encoded yet, this check will be false, for now.
150 // Once our token is encoded, OnTokenEncoded will call UpdateToken, which
151 // will call this code again (if we're still supposed to be playing).
152 SamplesMap::iterator samples
=
153 samples_caches_
[type
]->Get(playing_token_
[type
]);
154 if (samples
!= samples_caches_
[type
]->end()) {
155 DCHECK(!playing_token_
[type
].empty());
156 if (player_enabled_
[type
]) {
157 started_playing_
[type
] = base::Time::Now();
158 player_
[type
]->Play(samples
->second
);
160 // If we're playing, we always record to hear what we are playing.
163 DVLOG(3) << "Skipping playback for disabled " << AudioTypeToString(type
)
169 void ModemImpl::StopPlaying(AudioType type
) {
170 DCHECK(type
== AUDIBLE
|| type
== INAUDIBLE
);
171 should_be_playing_
[type
] = false;
172 player_
[type
]->Stop();
173 // If we were only recording to hear our own played tokens, stop.
174 if (!should_be_recording_
[AUDIBLE
] && !should_be_recording_
[INAUDIBLE
])
176 playing_token_
[type
] = std::string();
179 void ModemImpl::StartRecording(AudioType type
) {
180 DCHECK(type
== AUDIBLE
|| type
== INAUDIBLE
);
181 should_be_recording_
[type
] = true;
185 void ModemImpl::StopRecording(AudioType type
) {
186 DCHECK(type
== AUDIBLE
|| type
== INAUDIBLE
);
187 should_be_recording_
[type
] = false;
191 void ModemImpl::SetToken(AudioType type
,
192 const std::string
& url_safe_token
) {
193 DCHECK(type
== AUDIBLE
|| type
== INAUDIBLE
);
194 std::string token
= FromUrlSafe(url_safe_token
);
195 if (samples_caches_
[type
]->Get(token
) == samples_caches_
[type
]->end()) {
196 client_
->EncodeToken(token
, type
, token_params_
);
198 UpdateToken(type
, token
);
202 const std::string
ModemImpl::GetToken(AudioType type
) const {
203 return playing_token_
[type
];
206 bool ModemImpl::IsPlayingTokenHeard(AudioType type
) const {
207 base::TimeDelta tokenTimeout
=
208 base::TimeDelta::FromMilliseconds(kTokenTimeoutMs
);
210 // This is a bit of a hack. If we haven't been playing long enough,
211 // return true to avoid tripping an audio fail alarm.
212 if (base::Time::Now() - started_playing_
[type
] < tokenTimeout
)
215 return base::Time::Now() - heard_own_token_
[type
] < tokenTimeout
;
218 void ModemImpl::SetTokenParams(AudioType type
, const TokenParameters
& params
) {
219 DCHECK_GT(params
.length
, 0u);
220 token_params_
[type
] = params
;
222 // TODO(ckehoe): Make whispernet handle different token lengths
223 // simultaneously without reinitializing the decoder over and over.
227 scoped_ptr
<Modem
> Modem::Create() {
228 return make_scoped_ptr
<Modem
>(new ModemImpl
);
231 // Private functions.
233 void ModemImpl::OnTokenEncoded(
235 const std::string
& token
,
236 const scoped_refptr
<media::AudioBusRefCounted
>& samples
) {
237 samples_caches_
[type
]->Put(token
, samples
);
238 DumpToken(type
, token
, samples
.get());
239 UpdateToken(type
, token
);
242 void ModemImpl::OnTokensFound(const std::vector
<AudioToken
>& tokens
) {
243 std::vector
<AudioToken
> tokens_to_report
;
244 for (const auto& token
: tokens
) {
245 AudioType type
= token
.audible
? AUDIBLE
: INAUDIBLE
;
246 if (playing_token_
[type
] == token
.token
)
247 heard_own_token_
[type
] = base::Time::Now();
249 if (should_be_recording_
[AUDIBLE
] && token
.audible
) {
250 tokens_to_report
.push_back(token
);
251 } else if (should_be_recording_
[INAUDIBLE
] && !token
.audible
) {
252 tokens_to_report
.push_back(token
);
256 if (!tokens_to_report
.empty())
257 tokens_cb_
.Run(tokens_to_report
);
260 void ModemImpl::UpdateToken(AudioType type
, const std::string
& token
) {
261 DCHECK(type
== AUDIBLE
|| type
== INAUDIBLE
);
262 if (playing_token_
[type
] == token
)
266 playing_token_
[type
] = token
;
268 // If we are supposed to be playing this token type at this moment, switch
269 // out playback with the new samples.
270 if (should_be_playing_
[type
])
271 RestartPlaying(type
);
274 void ModemImpl::RestartPlaying(AudioType type
) {
275 DCHECK(type
== AUDIBLE
|| type
== INAUDIBLE
);
276 // We should already have this token in the cache. This function is not
277 // called from anywhere except update token and only once we have our samples
279 DCHECK(samples_caches_
[type
]->Get(playing_token_
[type
]) !=
280 samples_caches_
[type
]->end());
282 player_
[type
]->Stop();
286 void ModemImpl::DecodeSamplesConnector(const std::string
& samples
) {
287 // If we are either supposed to be recording *or* playing, audible or
288 // inaudible, we should be decoding that type. This is so that if we are
289 // just playing, we will still decode our recorded token so we can check
290 // if we heard our own token. Whether or not we report the token to the
291 // server is checked for and handled in OnTokensFound.
293 bool decode_audible
=
294 should_be_recording_
[AUDIBLE
] || should_be_playing_
[AUDIBLE
];
295 bool decode_inaudible
=
296 should_be_recording_
[INAUDIBLE
] || should_be_playing_
[INAUDIBLE
];
298 if (decode_audible
&& decode_inaudible
) {
299 client_
->DecodeSamples(BOTH
, samples
, token_params_
);
300 } else if (decode_audible
) {
301 client_
->DecodeSamples(AUDIBLE
, samples
, token_params_
);
302 } else if (decode_inaudible
) {
303 client_
->DecodeSamples(INAUDIBLE
, samples
, token_params_
);
307 void ModemImpl::DumpToken(AudioType audio_type
,
308 const std::string
& token
,
309 const media::AudioBus
* samples
) {
310 if (dump_tokens_dir_
.empty())
313 // Convert the samples to 16-bit integers.
314 std::vector
<int16_t> int_samples
;
315 int_samples
.reserve(samples
->frames());
316 for (int i
= 0; i
< samples
->frames(); i
++) {
317 int_samples
.push_back(round(
318 samples
->channel(0)[i
] * std::numeric_limits
<int16_t>::max()));
320 DCHECK_EQ(static_cast<int>(int_samples
.size()), samples
->frames());
321 DCHECK_EQ(kMonoChannelCount
, samples
->channels());
323 const std::string filename
= base::StringPrintf("%s %s.wav",
324 AudioTypeToString(audio_type
).c_str(), ToUrlSafe(token
).c_str());
325 DVLOG(3) << "Dumping token " << filename
;
327 std::string file_str
;
329 base::FilePath file_path
= dump_tokens_dir_
.Append(
330 base::SysNativeMBToWide(filename
));
331 file_str
= base::SysWideToNativeMB(file_path
.value());
333 file_str
= dump_tokens_dir_
.Append(filename
).value();
336 webrtc::WavWriter
writer(file_str
, kDefaultSampleRate
, kMonoChannelCount
);
337 writer
.WriteSamples(int_samples
.data(), int_samples
.size());
340 } // namespace audio_modem