diff --git a/js/module.d.ts b/js/module.d.ts index 5b27b3613..32fe3b64d 100644 --- a/js/module.d.ts +++ b/js/module.d.ts @@ -458,8 +458,12 @@ export interface IInput extends ISource { sendFocus(focus: boolean): void; sendKeyClick(eventData: IKeyEvent, keyUp: boolean): void; setFilterOrder(filter: IFilter, movement: EOrderMovement): void; - setFilterOrder(filter: IFilter, movement: EOrderMovement): void; + setFilterPosition(filter: IFilter, position: number): void; + setVideoFilterPosition(filter: IFilter, position: number): void; + setAudioFilterPosition(filter: IFilter, position: number): void; readonly filters: IFilter[]; + readonly videoFilters: IFilter[]; + readonly audioFilters: IFilter[]; readonly width: number; readonly height: number; getDuration(): number; diff --git a/js/module.ts b/js/module.ts index 2609b442f..7d70b8115 100644 --- a/js/module.ts +++ b/js/module.ts @@ -819,17 +819,27 @@ export interface IInput extends ISource { setFilterOrder(filter: IFilter, movement: EOrderMovement): void; /** - * Move a filter up, down, top, or bottom in the filter list. + * Set a filter position in the filter list. * @param filter - The filter to move within the input source. - * @param movement - The movement to make within the list. + * @param position - The position to make within the list. */ - setFilterOrder(filter: IFilter, movement: EOrderMovement): void; + setFilterPosition(filter: IFilter, position: number): void; + setVideoFilterPosition(filter: IFilter, position: number): void; + setAudioFilterPosition(filter: IFilter, position: number): void; /** * Obtain a list of all filters associated with the input source */ - readonly filters: IFilter[]; + readonly filters: IFilter[]; + /** + * Obtain a list of video filters associated with the input source + */ + readonly videoFilters: IFilter[]; + /** + * Obtain a list of audio filters associated with the input source + */ + readonly audioFilters: IFilter[]; /** * Width of the underlying source diff --git a/obs-studio-client/source/cache-manager.hpp b/obs-studio-client/source/cache-manager.hpp index 5d39965b2..f37a84f3d 100644 --- a/obs-studio-client/source/cache-manager.hpp +++ b/obs-studio-client/source/cache-manager.hpp @@ -45,7 +45,7 @@ struct SourceDataInfo uint32_t audioMixers = UINT32_MAX; bool audioMixersChanged = true; - std::vector* filters = new std::vector(); + std::vector>* filters = new std::vector>(); bool filtersOrderChanged = true; }; diff --git a/obs-studio-client/source/input.cpp b/obs-studio-client/source/input.cpp index 0096ca0f5..ba57e64aa 100644 --- a/obs-studio-client/source/input.cpp +++ b/obs-studio-client/source/input.cpp @@ -47,6 +47,9 @@ Napi::Object osn::Input::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("addFilter", &osn::Input::AddFilter), InstanceMethod("removeFilter", &osn::Input::RemoveFilter), InstanceMethod("setFilterOrder", &osn::Input::SetFilterOrder), + InstanceMethod("setFilterPosition", &osn::Input::SetFilterPosition), + InstanceMethod("setVideoFilterPosition", &osn::Input::SetVideoFilterPosition), + InstanceMethod("setAudioFilterPosition", &osn::Input::SetAudioFilterPosition), InstanceMethod("findFilter", &osn::Input::FindFilter), InstanceMethod("copyFilters", &osn::Input::CopyFilters), @@ -61,6 +64,8 @@ Napi::Object osn::Input::Init(Napi::Env env, Napi::Object exports) { InstanceAccessor("deinterlaceFieldOrder", &osn::Input::GetDeinterlaceFieldOrder, &osn::Input::SetDeinterlaceFieldOrder), InstanceAccessor("deinterlaceMode", &osn::Input::GetDeinterlaceMode, &osn::Input::SetDeinterlaceMode), InstanceAccessor("filters", &osn::Input::Filters, nullptr), + InstanceAccessor("videoFilters", &osn::Input::VideoFilters, nullptr), + InstanceAccessor("audioFilters", &osn::Input::AudioFilters, nullptr), InstanceAccessor("seek", &osn::Input::GetTime, &osn::Input::SetTime), InstanceAccessor("configurable", &osn::Input::CallIsConfigurable, nullptr), @@ -572,20 +577,44 @@ void osn::Input::SetDeinterlaceMode(const Napi::CallbackInfo& info, const Napi:: } Napi::Value osn::Input::Filters(const Napi::CallbackInfo& info) +{ + return GetFilters(info, osn::FilterSubset::All); +} + +Napi::Value osn::Input::AudioFilters(const Napi::CallbackInfo& info) +{ + return GetFilters(info, osn::FilterSubset::Audio); +} + +Napi::Value osn::Input::VideoFilters(const Napi::CallbackInfo& info) +{ + return GetFilters(info, osn::FilterSubset::Video); +} + +Napi::Value osn::Input::FiltersFromCache(const Napi::CallbackInfo& info, osn::FilterSubset subset, std::vector> * filters) +{ + Napi::Array array = Napi::Array::New(info.Env()); + int index = 0; + for (uint32_t i = 0; i < filters->size(); i++) { + if ((filters->at(i).second == static_cast(osn::FilterSubset::Video) && subset == osn::FilterSubset::Audio) + || (filters->at(i).second == static_cast(osn::FilterSubset::Audio) && subset == osn::FilterSubset::Video)) + continue; + auto instance = + osn::Filter::constructor.New({ + Napi::Number::New(info.Env(), filters->at(i).first) + }); + array.Set(index, instance); + index++; + } + return array; +} + +Napi::Value osn::Input::GetFilters(const Napi::CallbackInfo& info, osn::FilterSubset subset) { SourceDataInfo* sdi = CacheManager::getInstance().Retrieve(this->sourceId); if (sdi && !sdi->filtersOrderChanged) { - std::vector* filters = sdi->filters; - Napi::Array array = Napi::Array::New(info.Env(), int(filters->size())); - for (uint32_t i = 0; i < filters->size(); i++) { - auto instance = - osn::Filter::constructor.New({ - Napi::Number::New(info.Env(), filters->at(i)) - }); - array.Set(i, instance); - } - return array; + return FiltersFromCache(info, subset, sdi->filters); } auto conn = GetConnection(info); @@ -598,28 +627,35 @@ Napi::Value osn::Input::Filters(const Napi::CallbackInfo& info) if (!ValidateResponse(info, response)) return info.Env().Undefined(); - std::vector* filters; + Napi::Array array = Napi::Array::New(info.Env()); + std::vector>* filters; if (sdi) { filters = sdi->filters; filters->clear(); - } - Napi::Array array = Napi::Array::New(info.Env(), response.size() - 1); - for (size_t idx = 1; idx < response.size(); idx++) { - auto instance = - osn::Filter::constructor.New({ - Napi::Number::New(info.Env(), response[idx].value_union.ui64) - }); - array.Set(uint32_t(idx) - 1, instance); - - if (sdi) - filters->push_back(response[idx].value_union.ui64); - } + for (size_t idx = 1; idx < response.size(); idx += 2) { + filters->push_back(std::make_pair(response[idx].value_union.ui64, response[idx+1].value_union.i32)); + } - if (sdi) sdi->filtersOrderChanged = false; - return array; + return FiltersFromCache(info, subset, filters); + } else { + int index = 0; + for (size_t idx = 1; idx < response.size(); idx += 2) { + if ((response[idx+1].value_union.i32 == static_cast(osn::FilterSubset::Video) && subset == osn::FilterSubset::Audio) + || (response[idx+1].value_union.i32 == static_cast(osn::FilterSubset::Audio) && subset == osn::FilterSubset::Video)) + continue; + + auto instance = + osn::Filter::constructor.New({ + Napi::Number::New(info.Env(), response[idx].value_union.ui64) + }); + array.Set(index, instance); + index++; + } + return array; + } } Napi::Value osn::Input::AddFilter(const Napi::CallbackInfo& info) @@ -675,6 +711,66 @@ Napi::Value osn::Input::SetFilterOrder(const Napi::CallbackInfo& info) return info.Env().Undefined(); } +Napi::Value osn::Input::SetFilterPosition(const Napi::CallbackInfo& info) +{ + osn::Filter* objfilter = Napi::ObjectWrap::Unwrap(info[0].ToObject()); + uint32_t position = info[1].ToNumber().Uint32Value(); + + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + conn->call( + "Input", "PositionFilter", {ipc::value(this->sourceId), ipc::value(objfilter->sourceId), ipc::value(position), ipc::value(0)}); + + SourceDataInfo* sdi = CacheManager::getInstance().Retrieve(this->sourceId); + if (sdi) { + sdi->filtersOrderChanged = true; + } + + return info.Env().Undefined(); +} + +Napi::Value osn::Input::SetVideoFilterPosition(const Napi::CallbackInfo& info) +{ + osn::Filter* objfilter = Napi::ObjectWrap::Unwrap(info[0].ToObject()); + uint32_t position = info[1].ToNumber().Uint32Value(); + + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + conn->call( + "Input", "PositionFilter", {ipc::value(this->sourceId), ipc::value(objfilter->sourceId), ipc::value(position), ipc::value(static_cast(osn::FilterSubset::Video))}); + + SourceDataInfo* sdi = CacheManager::getInstance().Retrieve(this->sourceId); + if (sdi) { + sdi->filtersOrderChanged = true; + } + + return info.Env().Undefined(); +} + +Napi::Value osn::Input::SetAudioFilterPosition(const Napi::CallbackInfo& info) +{ + osn::Filter* objfilter = Napi::ObjectWrap::Unwrap(info[0].ToObject()); + uint32_t position = info[1].ToNumber().Uint32Value(); + + auto conn = GetConnection(info); + if (!conn) + return info.Env().Undefined(); + + conn->call( + "Input", "PositionFilter", {ipc::value(this->sourceId), ipc::value(objfilter->sourceId), ipc::value(position), ipc::value(static_cast(osn::FilterSubset::Video))}); + + SourceDataInfo* sdi = CacheManager::getInstance().Retrieve(this->sourceId); + if (sdi) { + sdi->filtersOrderChanged = true; + } + + return info.Env().Undefined(); +} + Napi::Value osn::Input::FindFilter(const Napi::CallbackInfo& info) { std::string name = info[0].ToString().Utf8Value(); diff --git a/obs-studio-client/source/input.hpp b/obs-studio-client/source/input.hpp index fde7aee98..b8ab00114 100644 --- a/obs-studio-client/source/input.hpp +++ b/obs-studio-client/source/input.hpp @@ -23,6 +23,12 @@ namespace osn { + enum class FilterSubset : std::int16_t + { + All =0, + Audio = 2, + Video = 1 + }; class Input : public Napi::ObjectWrap { public: @@ -43,6 +49,9 @@ namespace osn Napi::Value AddFilter(const Napi::CallbackInfo& info); Napi::Value RemoveFilter(const Napi::CallbackInfo& info); Napi::Value SetFilterOrder(const Napi::CallbackInfo& info); + Napi::Value SetFilterPosition(const Napi::CallbackInfo& info); + Napi::Value SetVideoFilterPosition(const Napi::CallbackInfo& info); + Napi::Value SetAudioFilterPosition(const Napi::CallbackInfo& info); Napi::Value FindFilter(const Napi::CallbackInfo& info); Napi::Value CopyFilters(const Napi::CallbackInfo& info); @@ -63,6 +72,10 @@ namespace osn Napi::Value GetDeinterlaceMode(const Napi::CallbackInfo& info); void SetDeinterlaceMode(const Napi::CallbackInfo& info, const Napi::Value &value); Napi::Value Filters(const Napi::CallbackInfo& info); + Napi::Value VideoFilters(const Napi::CallbackInfo& info); + Napi::Value AudioFilters(const Napi::CallbackInfo& info); + Napi::Value GetFilters(const Napi::CallbackInfo& info, osn::FilterSubset subset); + Napi::Value FiltersFromCache(const Napi::CallbackInfo& info, osn::FilterSubset subset, std::vector> * filters); Napi::Value CallIsConfigurable(const Napi::CallbackInfo& info); Napi::Value CallGetProperties(const Napi::CallbackInfo& info); diff --git a/obs-studio-server/source/osn-input.cpp b/obs-studio-server/source/osn-input.cpp index 7390e1069..c7bdd47ca 100644 --- a/obs-studio-server/source/osn-input.cpp +++ b/obs-studio-server/source/osn-input.cpp @@ -90,13 +90,15 @@ void osn::Input::Register(ipc::server& srv) "SetDeInterlaceMode", std::vector{ipc::type::UInt64, ipc::type::Int32}, GetDeInterlaceMode)); cls->register_function( - std::make_shared("GetFilters", std::vector{ipc::type::UInt64}, GetFilters)); + std::make_shared("GetFilters", std::vector{ipc::type::UInt64, ipc::type::Int32}, GetFilters)); cls->register_function(std::make_shared( "AddFilter", std::vector{ipc::type::UInt64, ipc::type::UInt64}, AddFilter)); cls->register_function(std::make_shared( "RemoveFilter", std::vector{ipc::type::UInt64, ipc::type::UInt64}, RemoveFilter)); cls->register_function(std::make_shared( "MoveFilter", std::vector{ipc::type::UInt64, ipc::type::UInt64, ipc::type::UInt32}, MoveFilter)); + cls->register_function(std::make_shared( + "PositionFilter", std::vector{ipc::type::UInt64, ipc::type::UInt64, ipc::type::UInt32, ipc::type::UInt32}, PositionFilter)); cls->register_function(std::make_shared( "FindFilter", std::vector{ipc::type::UInt64, ipc::type::String}, FindFilter)); cls->register_function(std::make_shared( @@ -632,6 +634,66 @@ void osn::Input::MoveFilter( AUTO_DEBUG; } +size_t FilterPositionInFullList(obs_source_t* input, size_t position, int subset) +{ + struct filter_position_info { + size_t count; + size_t position; + size_t subset_count; + size_t full_position; + int subset; + } info = {0, position, 0, position, subset}; + + auto enum_cb = [](obs_source_t* parent, obs_source_t* filter, void* data) { + filter_position_info* info = reinterpret_cast(data); + uint32_t output_flags = obs_source_get_output_flags(filter); + info->count++; + if(info->subset == OBS_SOURCE_VIDEO && (output_flags & OBS_SOURCE_VIDEO)) { + info->subset_count++; + if( info->subset_count == info->position) { + info->full_position = info->count; + } + } + if(info->subset == OBS_SOURCE_AUDIO && (output_flags & OBS_SOURCE_AUDIO)) { + info->subset_count++; + if( info->subset_count == info->position) { + info->full_position = info->count; + } + } + }; + + obs_source_enum_filters(input, enum_cb, &info); + return info.full_position; +} + +void osn::Input::PositionFilter( + void* data, + const int64_t id, + const std::vector& args, + std::vector& rval) +{ + obs_source_t* input = osn::Source::Manager::GetInstance().find(args[0].value_union.ui64); + if (!input) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Input reference is not valid."); + } + + obs_source_t* filter = osn::Source::Manager::GetInstance().find(args[1].value_union.ui64); + if (!filter) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Filter reference is not valid."); + } + + size_t position = (size_t)args[2].value_union.ui32; + int subset = args[3].value_union.ui32; + + if (subset != 0) + position = FilterPositionInFullList(input, position, subset); + + obs_source_filter_set_position(input, filter, position); + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + AUTO_DEBUG; +} + void osn::Input::FindFilter( void* data, const int64_t id, @@ -674,10 +736,12 @@ void osn::Input::GetFilters( auto enum_cb = [](obs_source_t* parent, obs_source_t* filter, void* data) { std::vector* rval = reinterpret_cast*>(data); - + uint32_t output_flags = obs_source_get_output_flags(filter); uint64_t id = osn::Source::Manager::GetInstance().find(filter); + if (id != UINT64_MAX) { rval->push_back(id); + rval->push_back(output_flags & (OBS_SOURCE_AUDIO|OBS_SOURCE_VIDEO)); } }; diff --git a/obs-studio-server/source/osn-input.hpp b/obs-studio-server/source/osn-input.hpp index ef83787e7..482f5df6a 100644 --- a/obs-studio-server/source/osn-input.hpp +++ b/obs-studio-server/source/osn-input.hpp @@ -128,6 +128,11 @@ namespace osn const int64_t id, const std::vector& args, std::vector& rval); + static void PositionFilter( + void* data, + const int64_t id, + const std::vector& args, + std::vector& rval); static void FindFilter( void* data, const int64_t id, diff --git a/tests/osn-tests/src/test_osn_input.ts b/tests/osn-tests/src/test_osn_input.ts index f76f474eb..e076e8348 100644 --- a/tests/osn-tests/src/test_osn_input.ts +++ b/tests/osn-tests/src/test_osn_input.ts @@ -563,7 +563,7 @@ describe(testName, () => { }); }); - it('Change the order of filters in the list', () => { + it('Change the order of filters in the list by moving', () => { // Creating source const input = osn.InputFactory.create(EOBSInputTypes.ImageSource, 'test_source'); @@ -620,6 +620,107 @@ describe(testName, () => { input.release(); }); + it('Change the order of filters in the list by positioning', () => { + // Creating source + const input = osn.InputFactory.create(EOBSInputTypes.FFMPEGSource, 'ffmpeg_source'); + + // Checking if source was created correctly + expect(input).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateInput, EOBSInputTypes.FFMPEGSource)); + expect(input.id).to.equal(EOBSInputTypes.FFMPEGSource, GetErrorMessage(ETestErrorMsg.InputId, EOBSInputTypes.FFMPEGSource)); + expect(input.name).to.equal('ffmpeg_source', GetErrorMessage(ETestErrorMsg.InputName, EOBSInputTypes.FFMPEGSource)); + + // Creating filters + const filter1 = osn.FilterFactory.create(EOBSFilterTypes.Color, 'filter1'); + const filter2 = osn.FilterFactory.create(EOBSFilterTypes.Crop, 'filter2'); + const filter3 = osn.FilterFactory.create(EOBSFilterTypes.Gain, 'filter3'); + const filter4 = osn.FilterFactory.create(EOBSFilterTypes.GPUDelay, 'filter4'); + + // Checking if filters were created correctly + expect(filter1).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Color)); + expect(filter2).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Crop)); + expect(filter3).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Gain)); + expect(filter4).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.GPUDelay)); + + // Adding filters to source + input.addFilter(filter1); + input.addFilter(filter2); + input.addFilter(filter3); + input.addFilter(filter4); + + // Checking if filters are in the right position + expect(input.filters[0].name).to.equal('filter1', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Color)); + expect(input.filters[1].name).to.equal('filter2', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Crop)); + expect(input.filters[2].name).to.equal('filter3', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Gain)); + expect(input.filters[3].name).to.equal('filter4', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.GPUDelay)); + + // Changing filter order down + input.setFilterOrder(filter1, osn.EOrderMovement.Down); + // Checking if filter is in the right position + expect(input.filters[1].name).to.equal('filter1', GetErrorMessage(ETestErrorMsg.MoveFilterDown, EOBSFilterTypes.Color)); + + // Change filter position + input.setFilterPosition(filter1, 2); + + // Checking if filter is in the right position + expect(input.filters[2].name).to.equal('filter1', GetErrorMessage(ETestErrorMsg.PositionFilter, EOBSFilterTypes.Color)); + + // Removing all filters + input.filters.forEach(function(filter) { + input.removeFilter(filter); + filter.release(); + }); + + input.release(); + }); + + it('Use separate lists for audio and video filters', () => { + // Creating source + const input = osn.InputFactory.create(EOBSInputTypes.FFMPEGSource, 'ffmpeg_source'); + + // Checking if source was created correctly + expect(input).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateInput, EOBSInputTypes.FFMPEGSource)); + expect(input.id).to.equal(EOBSInputTypes.FFMPEGSource, GetErrorMessage(ETestErrorMsg.InputId, EOBSInputTypes.FFMPEGSource)); + expect(input.name).to.equal('ffmpeg_source', GetErrorMessage(ETestErrorMsg.InputName, EOBSInputTypes.FFMPEGSource)); + + // Creating filters + const filter1 = osn.FilterFactory.create(EOBSFilterTypes.Color, 'filter1'); + const filter2 = osn.FilterFactory.create(EOBSFilterTypes.Crop, 'filter2'); + const filter3 = osn.FilterFactory.create(EOBSFilterTypes.Gain, 'filter3'); + const filter4 = osn.FilterFactory.create(EOBSFilterTypes.GPUDelay, 'filter4'); + const filter5 = osn.FilterFactory.create(EOBSFilterTypes.Compressor, 'filter5'); + + // Checking if filters were created correctly + expect(filter1).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Color)); + expect(filter2).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Crop)); + expect(filter3).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Gain)); + expect(filter4).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.GPUDelay)); + expect(filter5).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Compressor)); + + // Adding filters to source + input.addFilter(filter1); + input.addFilter(filter2); + input.addFilter(filter3); + input.addFilter(filter4); + + // Checking if filters are in the right position + expect(input.videoFilters[0].name).to.equal('filter1', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Color)); + expect(input.videoFilters[1].name).to.equal('filter2', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Crop)); + expect(input.videoFilters[2].name).to.equal('filter4', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.GPUDelay)); + expect(input.audioFilters[0].name).to.equal('filter3', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Gain)); + + // Adding more filters + input.addFilter(filter5); + expect(input.audioFilters[1].name).to.equal('filter5', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Compressor)); + + // Removing all filters + input.filters.forEach(function(filter) { + input.removeFilter(filter); + filter.release(); + }); + + input.release(); + }); + it('Fail test - Try to find an input that does not exist', () => { let inputFromName: IInput; diff --git a/tests/osn-tests/util/error_messages.ts b/tests/osn-tests/util/error_messages.ts index 7f426fb77..c4773dd4d 100644 --- a/tests/osn-tests/util/error_messages.ts +++ b/tests/osn-tests/util/error_messages.ts @@ -86,6 +86,8 @@ export const enum ETestErrorMsg { MonitoringType = 'Failed to update monitoring type of input %VALUE1%', FindFilter = 'Did not found filter %VALUE1% in input %VALUE2%', RemoveFilter = 'Not all filters were removed', + PositionFilter = 'Failed to move filter %VALUE1% into position', + FilterInsert = 'Failed to insert a filter %VALUE1% into source', MoveFilterDown = 'Failed to move filter %VALUE1% down', MoveFilterUp = 'Failed to move filter %VALUE1% up', MoveFilterBottom = 'Failed to move filter %VALUE1% to bottom',