1c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 3c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)// found in the LICENSE file. 4c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 5c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "media/base/android/webaudio_media_codec_bridge.h" 6c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 7c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include <errno.h> 8868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)#include <fcntl.h> 9868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)#include <sys/stat.h> 10868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)#include <sys/types.h> 11c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include <unistd.h> 12c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include <vector> 13c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 14c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "base/android/jni_android.h" 15c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "base/android/jni_array.h" 16c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "base/android/jni_string.h" 17c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "base/basictypes.h" 18c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "base/logging.h" 19c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "base/posix/eintr_wrapper.h" 204311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch#include "base/stl_util.h" 21c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "jni/WebAudioMediaCodecBridge_jni.h" 22c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "media/base/android/webaudio_media_codec_info.h" 23c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 24c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 25c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)using base::android::AttachCurrentThread; 26c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 27c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)namespace media { 28c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 29c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)void WebAudioMediaCodecBridge::RunWebAudioMediaCodec( 30c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) base::SharedMemoryHandle encoded_audio_handle, 31c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) base::FileDescriptor pcm_output, 32868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) uint32_t data_size) { 33c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) WebAudioMediaCodecBridge bridge(encoded_audio_handle, pcm_output, data_size); 34c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 35c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) bridge.DecodeInMemoryAudioFile(); 36c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)} 37c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 38c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)WebAudioMediaCodecBridge::WebAudioMediaCodecBridge( 39c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) base::SharedMemoryHandle encoded_audio_handle, 40c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) base::FileDescriptor pcm_output, 41868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) uint32_t data_size) 42868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) : encoded_audio_handle_(encoded_audio_handle), 43c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) pcm_output_(pcm_output.fd), 44c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) data_size_(data_size) { 45c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) DVLOG(1) << "WebAudioMediaCodecBridge start **********************" 46c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) << " output fd = " << pcm_output.fd; 47c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)} 48c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 49c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)WebAudioMediaCodecBridge::~WebAudioMediaCodecBridge() { 50c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) if (close(pcm_output_)) { 51c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) DVLOG(1) << "Couldn't close output fd " << pcm_output_ 52c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) << ": " << strerror(errno); 53c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) } 54868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)} 55c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 56868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)int WebAudioMediaCodecBridge::SaveEncodedAudioToFile( 57868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) JNIEnv* env, 58868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) jobject context) { 59868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) // Create a temporary file where we can save the encoded audio data. 60868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) std::string temporaryFile = 61868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) base::android::ConvertJavaStringToUTF8( 62868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) env, 63868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) Java_WebAudioMediaCodecBridge_CreateTempFile(env, context).obj()); 64868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 65868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) // Open the file and unlink it, so that it will be actually removed 66868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) // when we close the file. 67868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) int fd = open(temporaryFile.c_str(), O_RDWR); 68868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) if (unlink(temporaryFile.c_str())) { 69868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) VLOG(0) << "Couldn't unlink temp file " << temporaryFile 70868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) << ": " << strerror(errno); 71868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) } 72868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 73868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) if (fd < 0) { 74868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) return -1; 75868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) } 76868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 77868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) // Create a local mapping of the shared memory containing the 78868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) // encoded audio data, and save the contents to the temporary file. 79868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) base::SharedMemory encoded_data(encoded_audio_handle_, true); 80868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 81868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) if (!encoded_data.Map(data_size_)) { 82868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) VLOG(0) << "Unable to map shared memory!"; 83868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) return -1; 84868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) } 85868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 86868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) if (static_cast<uint32_t>(write(fd, encoded_data.memory(), data_size_)) 87868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) != data_size_) { 88868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) VLOG(0) << "Failed to write all audio data to temp file!"; 89868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) return -1; 90c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) } 91868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 92868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) lseek(fd, 0, SEEK_SET); 93868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 94868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) return fd; 95c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)} 96c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 97c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)bool WebAudioMediaCodecBridge::DecodeInMemoryAudioFile() { 98c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) JNIEnv* env = AttachCurrentThread(); 99c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) CHECK(env); 100868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 101868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) jobject context = base::android::GetApplicationContext(); 102868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 103868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) int sourceFd = SaveEncodedAudioToFile(env, context); 104868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 105868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) if (sourceFd < 0) 106868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) return false; 107868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 108c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) jboolean decoded = Java_WebAudioMediaCodecBridge_decodeAudioFile( 109c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) env, 110868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) context, 111c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) reinterpret_cast<intptr_t>(this), 112868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) sourceFd, 113c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) data_size_); 114c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 115868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) close(sourceFd); 116868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 117868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) DVLOG(1) << "decoded = " << (decoded ? "true" : "false"); 118868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles) 119c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) return decoded; 120c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)} 121c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 122c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)void WebAudioMediaCodecBridge::InitializeDestination( 123c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) JNIEnv* env, 124c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) jobject /*java object*/, 125c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) jint channel_count, 126c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) jint sample_rate, 127c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) jlong duration_microsec) { 128c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // Send information about this audio file: number of channels, 129c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // sample rate (Hz), and the number of frames. 130c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) struct WebAudioMediaCodecInfo info = { 131c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) static_cast<unsigned long>(channel_count), 132c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) static_cast<unsigned long>(sample_rate), 133c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // The number of frames is the duration of the file 134c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // (in microseconds) times the sample rate. 135c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) static_cast<unsigned long>( 136c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 0.5 + (duration_microsec * 0.000001 * 137c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) sample_rate)) 138c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) }; 139c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 140c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) DVLOG(1) << "InitializeDestination:" 141c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) << " channel count = " << channel_count 142c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) << " rate = " << sample_rate 143c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) << " duration = " << duration_microsec << " microsec"; 144c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 145c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) HANDLE_EINTR(write(pcm_output_, &info, sizeof(info))); 146c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)} 147c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 148c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)void WebAudioMediaCodecBridge::OnChunkDecoded( 149c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) JNIEnv* env, 150c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) jobject /*java object*/, 151c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) jobject buf, 1524311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch jint buf_size, 1534311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch jint input_channel_count, 1544311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch jint output_channel_count) { 155c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 156c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) if (buf_size <= 0 || !buf) 157c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) return; 158c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 159c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) int8_t* buffer = 160c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) static_cast<int8_t*>(env->GetDirectBufferAddress(buf)); 161c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) size_t count = static_cast<size_t>(buf_size); 1624311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch std::vector<int16_t> decoded_data; 1634311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch 1644311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch if (input_channel_count == 1 && output_channel_count == 2) { 1654311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch // See crbug.com/266006. The file has one channel, but the 1664311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch // decoder decided to return two channels. To be consistent with 1674311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch // the number of channels in the file, only send one channel (the 1684311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch // first). 1694311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch int16_t* data = static_cast<int16_t*>(env->GetDirectBufferAddress(buf)); 1704311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch int frame_count = buf_size / sizeof(*data) / 2; 1714311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch 1724311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch decoded_data.resize(frame_count); 1734311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch for (int k = 0; k < frame_count; ++k) { 1744311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch decoded_data[k] = *data; 1754311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch data += 2; 1764311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch } 1774311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch buffer = reinterpret_cast<int8_t*>(vector_as_array(&decoded_data)); 1784311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch DCHECK(buffer); 1794311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch count = frame_count * sizeof(*data); 1804311e82a78ceafbe0585f51d4c8a86df9f21aa0dBen Murdoch } 181c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 182c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // Write out the data to the pipe in small chunks if necessary. 183c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) while (count > 0) { 184c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) int bytes_to_write = (count >= PIPE_BUF) ? PIPE_BUF : count; 185c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) ssize_t bytes_written = HANDLE_EINTR(write(pcm_output_, 186c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) buffer, 187c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) bytes_to_write)); 188c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) if (bytes_written == -1) 189c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) break; 190c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) count -= bytes_written; 191c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) buffer += bytes_written; 192c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) } 193c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)} 194c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 195c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)bool WebAudioMediaCodecBridge::RegisterWebAudioMediaCodecBridge(JNIEnv* env) { 196c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) return RegisterNativesImpl(env); 197c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)} 198c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 199c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)} // namespace 200