Implement plugin browser, with search and favorites.

This commit is contained in:
Jonathan Moore Liles 2013-04-13 13:18:03 -07:00
parent 5d35f37d4e
commit dd9eb35ab2
9 changed files with 648 additions and 136 deletions

View File

@ -292,6 +292,12 @@ LADSPAInfo::GetMenuList(void)
return m_SSMMenuList;
}
const vector<LADSPAInfo::PluginInfo>
LADSPAInfo::GetPluginInfo(void)
{
return m_Plugins;
}
unsigned long
LADSPAInfo::GetPluginListEntryByID(unsigned long unique_id)
{
@ -378,7 +384,7 @@ LADSPAInfo::DescendGroup(string prefix,
pe.Depth = depth;
pe.UniqueID = pi->UniqueID;
pe.Name = prefix + name;
plugins.push_back(pe);
}
plugins.sort();
@ -599,6 +605,13 @@ LADSPAInfo::ExaminePluginLibrary(const string path,
library_added = true;
}
if ( ! desc->Name )
{
printf( "WARNING: LADSPA Plugin with id %lu has no name!\n", desc->UniqueID );
continue;
}
// Add plugin info
PluginInfo pi;
pi.LibraryIndex = m_Libraries.size() - 1;
@ -607,19 +620,34 @@ LADSPAInfo::ExaminePluginLibrary(const string path,
pi.Label = desc->Label;
pi.Name = desc->Name;
pi.Descriptor = NULL;
m_Plugins.push_back(pi);
pi.Maker = desc->Maker;
pi.AudioInputs = 0;
pi.AudioOutputs = 0;
// Find number of input ports
unsigned long in_port_count = 0;
for (unsigned long p = 0; p < desc->PortCount; p++) {
if (LADSPA_IS_PORT_INPUT(desc->PortDescriptors[p])) {
in_port_count++;
}
if (LADSPA_IS_PORT_INPUT(desc->PortDescriptors[p])) {
in_port_count++;
if ( LADSPA_IS_PORT_AUDIO(desc->PortDescriptors[p] ) )
pi.AudioInputs++;
}
}
for (unsigned long p = 0; p < desc->PortCount; p++) {
if (LADSPA_IS_PORT_OUTPUT(desc->PortDescriptors[p])) {
if ( LADSPA_IS_PORT_AUDIO(desc->PortDescriptors[p] ) )
pi.AudioOutputs++;
}
}
if (in_port_count > m_MaxInputPortCount) {
m_MaxInputPortCount = in_port_count;
m_MaxInputPortCount = in_port_count;
}
m_Plugins.push_back(pi);
// Add to index
m_IDLookup[desc->UniqueID] = m_Plugins.size() - 1;

View File

@ -97,8 +97,27 @@ public:
}
};
// For cached plugin information
struct PluginInfo
{
unsigned long LibraryIndex; // Index of library in m_Libraries
unsigned long Index; // Plugin index in library
unsigned long UniqueID; // Unique ID
std::string Label; // Plugin label
std::string Name; // Plugin Name
std::string Maker;
unsigned int AudioInputs;
unsigned int AudioOutputs;
const LADSPA_Descriptor *Descriptor; // Descriptor, NULL
};
// Get ordered list of plugin names and IDs for plugin menu
const std::vector<PluginEntry> GetMenuList(void);
const std::vector<PluginInfo> GetPluginInfo(void);
// Get the index in the above list for given Unique ID
// If not found, this returns the size of the above list
@ -140,17 +159,6 @@ private:
void *Handle; // DLL Handle, NULL
};
// For cached plugin information
struct PluginInfo
{
unsigned long LibraryIndex; // Index of library in m_Libraries
unsigned long Index; // Plugin index in library
unsigned long UniqueID; // Unique ID
std::string Label; // Plugin label
std::string Name; // Plugin Name
const LADSPA_Descriptor *Descriptor; // Descriptor, NULL
};
// For cached RDF uri information
struct RDFURIInfo
{

View File

@ -39,6 +39,7 @@
#include "FL/menu_popup.H"
#include "Mixer.H"
#include "Plugin_Chooser.H"
#include "OSC/Endpoint.H"
#include "string_util.h"
@ -602,61 +603,55 @@ Module::draw_label ( void )
void
Module::insert_menu_cb ( const Fl_Menu_ *m )
{
if ( ! m->mvalue() || m->mvalue()->flags & FL_SUBMENU_POINTER || m->mvalue()->flags & FL_SUBMENU )
return;
void * v = m->mvalue()->user_data();
if ( v )
unsigned long id = Plugin_Chooser::plugin_chooser( this->ninputs() );
Module *mod = NULL;
switch ( id )
{
unsigned long id = *((unsigned long *)v);
Module *mod = NULL;
switch ( id )
case 0:
return;
case 1:
mod = new JACK_Module();
break;
case 2:
mod = new Gain_Module();
break;
case 3:
mod = new Meter_Module();
break;
case 4:
mod = new Mono_Pan_Module();
break;
default:
{
case 1:
mod = new JACK_Module();
break;
case 2:
mod = new Gain_Module();
break;
case 3:
mod = new Meter_Module();
break;
case 4:
mod = new Mono_Pan_Module();
break;
default:
{
Plugin_Module *m = new Plugin_Module();
Plugin_Module *m = new Plugin_Module();
m->load( id );
m->load( id );
mod = m;
}
mod = m;
}
}
if ( mod )
{
if ( !strcmp( mod->name(), "JACK" ) )
{
DMESSAGE( "Special casing JACK module" );
JACK_Module *jm = (JACK_Module*)mod;
jm->chain( chain() );
jm->configure_inputs( ninputs() );
jm->configure_outputs( ninputs() );
}
if ( mod )
if ( ! chain()->insert( this, mod ) )
{
if ( !strcmp( mod->name(), "JACK" ) )
{
DMESSAGE( "Special casing JACK module" );
JACK_Module *jm = (JACK_Module*)mod;
jm->chain( chain() );
jm->configure_inputs( ninputs() );
jm->configure_outputs( ninputs() );
}
if ( ! chain()->insert( this, mod ) )
{
fl_alert( "Cannot insert this module at this point in the chain" );
delete mod;
return;
}
redraw();
fl_alert( "Cannot insert this module at this point in the chain" );
delete mod;
return;
}
redraw();
}
}
@ -733,7 +728,10 @@ Module::menu ( void ) const
insert_menu->add( "Meter", 0, 0, new unsigned long(3) );
insert_menu->add( "Mono Pan", 0, 0, new unsigned long(4) );
Plugin_Module::add_plugins_to_menu( insert_menu );
insert_menu->add( "Plugin", 0, 0, new unsigned long(4) );
/* Plugin_Module::add_plugins_to_menu( insert_menu ); */
// menu_set_callback( insert_menu, &Module::insert_menu_cb, (void*)this );
insert_menu->callback( &Module::insert_menu_cb, (void*)this );

384
mixer/src/Plugin_Chooser.C Normal file
View File

@ -0,0 +1,384 @@
/*******************************************************************************/
/* Copyright (C) 2013 Jonathan Moore Liles */
/* */
/* This program is free software; you can redistribute it and/or modify it */
/* under the terms of the GNU General Public License as published by the */
/* Free Software Foundation; either version 2 of the License, or (at your */
/* option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, but WITHOUT */
/* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
/* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for */
/* more details. */
/* */
/* You should have received a copy of the GNU General Public License along */
/* with This program; see the file COPYING. If not,write to the Free Software */
/* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */
/*******************************************************************************/
#include <FL/Fl.H>
#include <FL/Fl_Browser.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Scalepack.H>
#include "Plugin_Chooser_UI.H"
#include "Plugin_Module.H"
#include "Plugin_Chooser.H"
#include "stdio.h"
#include <FL/Fl_Box.H>
#include <FL/fl_draw.H>
#include <algorithm>
static std::vector <Plugin_Module::Plugin_Info*> _plugin_rows;
unsigned long
Plugin_Chooser::plugin_chooser ( int ninputs )
{
Plugin_Chooser *o = new Plugin_Chooser( 0,0,735,500,"Plugin Chooser");
o->ui->inputs_input->value( ninputs );
o->search( "", "", ninputs, 0, o->ui->favorites_button->value() );
o->show();
while ( o->shown() )
Fl::wait();
unsigned long picked = o->value();
delete o;
return picked;
}
void
Plugin_Chooser::search ( const char *name, const char *author, int ninputs, int noutputs, bool favorites )
{
_plugin_rows.clear();
for ( std::list<Plugin_Module::Plugin_Info>::iterator i = _plugins.begin(); i != _plugins.end(); i++ )
{
Plugin_Module::Plugin_Info *p = &(*i);
if ( strcasestr( p->name, name ) &&
strcasestr( p->author, author ) )
{
if ( !
((( ( ninputs == 0 || ninputs == p->audio_inputs ) ) &&
( noutputs == 0 || noutputs == p->audio_outputs )) ||
( p->audio_inputs == 1 && p->audio_outputs == 1 ) ) )
continue;
if ( p->audio_outputs == 0 || p->audio_inputs == 0 )
/* we don't support these */
continue;
if ( favorites > 0 && ! p->favorite )
continue;
_plugin_rows.push_back( p );
}
}
ui->table->rows( _plugin_rows.size() );
}
void
Plugin_Chooser::cb_handle ( Fl_Widget *w, void *v )
{
((Plugin_Chooser*)v)->cb_handle( w );
}
void
Plugin_Chooser::cb_handle ( Fl_Widget *w )
{
if ( w == ui->all_button )
{
ui->favorites_button->value( !ui->all_button->value() );
}
{
search( ui->name_input->value(), ui->author_input->value(), ui->inputs_input->value(), ui->outputs_input->value(), ui->favorites_button->value() );
}
}
class Plugin_Table : public Fl_Table_Row
{
protected:
void draw_cell(TableContext context, // table cell drawing
int R=0, int C=0, int X=0, int Y=0, int W=0, int H=0);
public:
Plugin_Table(int x, int y, int w, int h, const char *l=0) : Fl_Table_Row(x,y,w,h,l)
{
end();
}
~Plugin_Table() { }
};
void Plugin_Table::draw_cell(TableContext context,
int R, int C, int X, int Y, int W, int H)
{
const char *headings[] = { "Fav.", "Name", "Author", "Type", "In", "Out" };
static char s[40];
switch ( context )
{
case CONTEXT_STARTPAGE:
fl_font(FL_HELVETICA, 12);
return;
case CONTEXT_COL_HEADER:
fl_push_clip(X, Y, W, H);
{
fl_draw_box(FL_THIN_UP_BOX, X, Y, W, H, col_header_color());
fl_color(FL_FOREGROUND_COLOR);
fl_draw(headings[C], X, Y, W, H, FL_ALIGN_CENTER);
}
fl_pop_clip();
return;
case CONTEXT_ROW_HEADER:
return;
case CONTEXT_CELL:
{
const char *s2 = (char*)s;
Fl_Align a = FL_ALIGN_CENTER;
int symbol = 0;
switch ( C )
{
case 0:
sprintf( s, "%s", _plugin_rows[R]->favorite ? "@circle" : "" );
symbol = 1;
break;
case 1:
a = FL_ALIGN_LEFT;
s2 = _plugin_rows[R]->name;
break;
case 2:
a = FL_ALIGN_LEFT;
s2 = _plugin_rows[R]->author;
break;
case 3:
s2 = _plugin_rows[R]->type;
break;
case 4:
sprintf( s, "%i", _plugin_rows[R]->audio_inputs );
break;
case 5:
sprintf( s, "%i", _plugin_rows[R]->audio_outputs );
break;
}
fl_push_clip(X, Y, W, H);
{
// BG COLOR
fl_color( row_selected(R) ? selection_color() : FL_DARK1);
fl_rectf(X, Y, W, H);
// TEXT
fl_color(FL_FOREGROUND_COLOR);
fl_draw(s2, X, Y, W, H, a, 0, symbol );
// BORDER
fl_color(color());
fl_rect(X, Y, W, H);
}
fl_pop_clip();
return;
}
case CONTEXT_TABLE:
fprintf(stderr, "TABLE CONTEXT CALLED\n");
return;
case CONTEXT_ENDPAGE:
case CONTEXT_RC_RESIZE:
case CONTEXT_NONE:
return;
}
}
void
Plugin_Chooser::cb_table ( Fl_Widget *w, void *v )
{
((Plugin_Chooser*)v)->cb_table(w);
}
void
Plugin_Chooser::cb_table ( Fl_Widget *w )
{
Fl_Table_Row *o = (Fl_Table_Row*)w;
int R = o->callback_row();
int C = o->callback_col();
Fl_Table::TableContext context = o->callback_context();
if ( context == Fl_Table::CONTEXT_CELL )
{
if ( C == 0 )
{
_plugin_rows[R]->favorite = ! _plugin_rows[R]->favorite;
o->redraw();
}
else
{
_value = _plugin_rows[R]->id;
hide();
}
}
}
extern char *user_config_dir;
static FILE *open_favorites( const char *mode )
{
char *path;
asprintf( &path, "%s/%s", user_config_dir, "favorite_plugins" );
FILE *fp = fopen( path, mode );
free( path );
return fp;
}
int
Plugin_Chooser::load_favorites ( void )
{
FILE *fp = open_favorites( "r" );
if ( !fp )
{
return 0;
}
unsigned long id;
char *type;
int favorites = 0;
while ( 2 == fscanf( fp, "%a[^:]:%lu\n", &type, &id ) )
{
for ( std::list<Plugin_Module::Plugin_Info>::iterator i = _plugins.begin();
i != _plugins.end();
i++ )
{
if ( !strcmp( (*i).type, type ) &&
(*i).id == id )
{
(*i).favorite = 1;
favorites++;
}
}
free(type);
}
fclose(fp);
return favorites;
}
void
Plugin_Chooser::save_favorites ( void )
{
FILE *fp = open_favorites( "w" );
if ( !fp )
return;
for ( std::list<Plugin_Module::Plugin_Info>::iterator i = _plugins.begin();
i != _plugins.end();
i++ )
{
if ( (*i).favorite )
{
fprintf( fp, "%s:%lu\n", i->type, i->id );
}
}
fclose( fp );
}
Plugin_Chooser::Plugin_Chooser ( int X,int Y,int W,int H, const char *L )
: Fl_Double_Window ( X,Y,W,H,L )
{
set_modal();
_value = 0;
_plugins = Plugin_Module::get_all_plugins();
{
Plugin_Chooser_UI *o = ui = new Plugin_Chooser_UI(X,Y,W,H);
o->name_input->callback( &Plugin_Chooser::cb_handle, this );
o->name_input->when( FL_WHEN_CHANGED );
o->author_input->callback( &Plugin_Chooser::cb_handle, this );
o->author_input->when( FL_WHEN_CHANGED );
o->inputs_input->callback( &Plugin_Chooser::cb_handle, this );
o->inputs_input->when( FL_WHEN_CHANGED );
o->outputs_input->callback( &Plugin_Chooser::cb_handle, this );
o->outputs_input->when( FL_WHEN_CHANGED );
o->favorites_button->callback( &Plugin_Chooser::cb_handle, this );
o->favorites_button->when( FL_WHEN_CHANGED );
o->all_button->callback( &Plugin_Chooser::cb_handle, this );
o->all_button->when( FL_WHEN_CHANGED );
{
Plugin_Table *o = new Plugin_Table(ui->table->x(),ui->table->y(),ui->table->w(),ui->table->h() );
ui->table_group->add(o);
ui->table_group->resizable(o);
delete ui->table;
ui->table = o;
/* ui->scalepack->add( o ); */
/* ui->scalepack->resizable( o ); */
o->col_header(1);
o->col_resize(1);
o->row_resize(1);
o->cols(6);
o->col_resize_min(4);
o->col_width(0,30);
o->col_width(1,350 - 7);
o->col_width(2,200);
o->col_width(3,75);
o->col_width(4,30);
o->col_width(5,30);
o->color(FL_BLACK);
o->box(FL_NO_BOX);
o->when(FL_WHEN_CHANGED);
o->callback( &Plugin_Chooser::cb_table, this );
}
resizable(o);
}
size_range( 735, 300, 735, 0 );
end();
if ( load_favorites() )
{
ui->all_button->value(0);
ui->favorites_button->value(1);
}
}
Plugin_Chooser::~Plugin_Chooser( )
{
save_favorites();
}

View File

@ -0,0 +1,57 @@
/*******************************************************************************/
/* Copyright (C) 2013 Jonathan Moore Liles */
/* */
/* This program is free software; you can redistribute it and/or modify it */
/* under the terms of the GNU General Public License as published by the */
/* Free Software Foundation; either version 2 of the License, or (at your */
/* option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, but WITHOUT */
/* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
/* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for */
/* more details. */
/* */
/* You should have received a copy of the GNU General Public License along */
/* with This program; see the file COPYING. If not,write to the Free Software */
/* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */
/*******************************************************************************/
#pragma once
#include <FL/Fl_Double_Window.H>
#include "Plugin_Module.H"
#include <vector>
class Plugin_Chooser_UI;
class Plugin_Chooser : public Fl_Double_Window
{
Plugin_Chooser_UI *ui;
std::list <Plugin_Module::Plugin_Info> _plugins;
static void cb_handle ( Fl_Widget *w, void *v );
void cb_handle ( Fl_Widget *w );
static void cb_table ( Fl_Widget *w, void *v );
void cb_table ( Fl_Widget *w );
void search ( const char *name, const char *author, int ninputs, int noutputs, bool favorites );
unsigned long _value;
int load_favorites ( void );
void save_favorites ( void );
public:
unsigned long value ( void ) const { return _value; }
Plugin_Chooser ( int X,int Y,int W,int H, const char *L=0 );
virtual ~Plugin_Chooser( );
static unsigned long plugin_chooser ( int ninputs );
};

View File

@ -0,0 +1,64 @@
# data file for the Fltk User Interface Designer (fluid)
version 1.0300
header_name {.h}
code_name {.cxx}
decl {\#include <FL/Fl_Scalepack.H>} {public global
}
decl {\#include <FL/Fl_Table_Row.H>} {public global
}
widget_class Plugin_Chooser_UI {
label {Plugin Selector} open selected
xywh {821 343 735 500} type Double resizable size_range {740 0 740 0} visible
} {
Fl_Group {} {open
xywh {10 5 155 20}
} {
Fl_Round_Button all_button {
label All
xywh {10 5 50 20} type Radio down_box ROUND_DOWN_BOX value 1
}
Fl_Round_Button favorites_button {
label Favorites
xywh {65 5 100 20} type Radio down_box ROUND_DOWN_BOX
}
}
Fl_Group {} {open
xywh {5 29 725 77} box UP_FRAME
code0 {o->resizable(0);}
} {
Fl_Input name_input {
label {Name:}
xywh {65 38 555 24} labelsize 12 textsize 13
}
Fl_Input author_input {
label {Author:}
xywh {65 72 415 24} labelsize 12 textsize 13
}
Fl_Value_Input outputs_input {
label {Outputs:}
xywh {693 70 30 26} labelsize 12
}
Fl_Value_Input inputs_input {
label {Inputs:}
xywh {693 39 30 24} labelsize 12
}
}
Fl_Group table_group {open
xywh {5 115 725 380} resizable
} {
Fl_Table table {open
xywh {5 115 725 380} resizable
} {}
}
Fl_Choice type_choice {
label {Type:} open
xywh {520 135 100 28} down_box BORDER_BOX labelsize 12 hide
} {
MenuItem {} {
label LADSPA
xywh {0 -68 34 18}
}
}
}

View File

@ -45,6 +45,8 @@
#include <dsp.h>
#include <algorithm>
static LADSPAInfo *ladspainfo;
@ -119,64 +121,6 @@ Plugin_Module::set ( Log_Entry &e )
void
Plugin_Module::add_plugins_to_menu ( Fl_Menu_Button *menu )
{
Plugin_Module::Plugin_Info *pia = Plugin_Module::get_all_plugins();
char path[1024];
for ( Plugin_Module::Plugin_Info *pi = pia; pi->path; ++pi )
{
snprintf( path, sizeof( path ), "Plugin/%s", pi->path );
menu->add(path, 0, NULL, new unsigned long( pi->id ), 0 );
}
delete[] pia;
}
/* allow the user to pick a plugin */
Plugin_Module *
Plugin_Module::pick_plugin ( void )
{
/**************/
/* build menu */
/**************/
Fl_Menu_Button *menu = new Fl_Menu_Button( 0, 0, 400, 400 );
menu->type( Fl_Menu_Button::POPUP3 );
Plugin_Module::Plugin_Info *pia = Plugin_Module::get_all_plugins();
for ( Plugin_Module::Plugin_Info *pi = pia; pi->path; ++pi )
{
menu->add(pi->path, 0, NULL, pi, 0 );
}
menu->popup();
if ( menu->value() <= 0 )
return NULL;
/************************/
/* load selected plugin */
/************************/
Plugin_Module::Plugin_Info *pi = (Plugin_Module::Plugin_Info*)menu->menu()[ menu->value() ].user_data();
if ( ! pi )
return NULL;
Plugin_Module *m = new Plugin_Module();
m->load( pi->id );
delete[] pia;
return m;
}
void
Plugin_Module::init ( void )
{
@ -319,7 +263,7 @@ Plugin_Module::join_discover_thread ( void )
}
/* return a list of available plugins */
Plugin_Module::Plugin_Info *
std::list<Plugin_Module::Plugin_Info>
Plugin_Module::get_all_plugins ( void )
{
if ( !ladspainfo )
@ -330,19 +274,30 @@ Plugin_Module::get_all_plugins ( void )
plugin_discover_thread->join();
}
std::vector<LADSPAInfo::PluginEntry> plugins = ladspainfo->GetMenuList();
std::vector<LADSPAInfo::PluginInfo> plugins = ladspainfo->GetPluginInfo();
Plugin_Info* pi = new Plugin_Info[plugins.size() + 1];
std::list<Plugin_Module::Plugin_Info> pr;
int j = 0;
for (std::vector<LADSPAInfo::PluginEntry>::iterator i=plugins.begin();
for (std::vector<LADSPAInfo::PluginInfo>::iterator i=plugins.begin();
i!=plugins.end(); i++, j++)
{
pi[j].path = i->Name.c_str();
pi[j].id = i->UniqueID;
Plugin_Info pi;
// pi[j].path = i->Name.c_str();
pi.path = NULL;
pi.id = i->UniqueID;
pi.author = i->Maker.c_str();
pi.name = i->Name.c_str();
pi.audio_inputs = i->AudioInputs;
pi.audio_outputs = i->AudioOutputs;
pr.push_back( pi );
}
return pi;
pr.sort();
return pr;
}
bool

View File

@ -35,12 +35,30 @@ public:
{
const char *path;
unsigned long id;
const char *name;
const char *author;
const char *category;
int audio_inputs;
int audio_outputs;
const char *type;
bool favorite;
Plugin_Info ( )
{
path = 0;
id = 0;
name = 0;
author = 0;
category = 0;
audio_inputs = 0;
audio_outputs = 0;
type = "LADSPA";
favorite = 0;
}
bool operator< ( const Plugin_Info &rhs ) {
return strcmp( name, rhs.name ) < 1;
}
};
bool load ( unsigned long id );
@ -71,8 +89,7 @@ private:
bool _crosswire;
static void *discover_thread ( void * );
static Plugin_Info* get_all_plugins ( void );
void set_input_buffer ( int n, void *buf );
void set_output_buffer ( int n, void *buf );
@ -89,15 +106,14 @@ private:
public:
static std::list<Plugin_Info> get_all_plugins ( void );
static void spawn_discover_thread ( void );
static void join_discover_thread ( void );
Plugin_Module ( );
virtual ~Plugin_Module();
static Plugin_Module * pick_plugin ( void );
static void add_plugins_to_menu ( Fl_Menu_Button *menu );
int plugin_ins ( void ) const { return _plugin_ins; }
int plugin_outs ( void ) const { return _plugin_outs; }

View File

@ -57,6 +57,8 @@ src/Mixer_Strip.C
src/Module.C
src/Module_Parameter_Editor.C
src/Mono_Pan_Module.C
src/Plugin_Chooser_UI.fl
src/Plugin_Chooser.C
src/NSM.C
src/Panner.C
src/Plugin_Module.C