'. t('The audio module allows users to upload and store audio files on a Drupal site. Audio is an important medium for community communication as the recent rise of the podcast phenomenon demonstrates.', array('!elink-en-wikipedia-org' => 'http://en.wikipedia.org/wiki/Podcasting')) .'
'; $help .= ''. t('Users create audio nodes by uploading a file from their computer. They are then able to make changes to the metadata, perhaps adding an artist, or removing the track number. Visitors can download the audio file, view the file\'s metadata and encoding information, or browse for audio by metadata (artist, title, year, etc). Visitors can even play MP3s within their browser using the XSPF flash player that is bundled with the module.', array('!elink-musicplayer-sourceforge-net' => 'http://musicplayer.sourceforge.net/')) .'
'; $help .= ''. t('The module uses the getID3 library to read and write ID3 tag information from the audio file. getID3 can read metadata from a many different audio and video formats giving the audio module a great deal of flexibility.', array('!elink-www-getid3-org' => 'http://www.getid3.org', '!elink-en-wikipedia-org' => 'http://en.wikipedia.org/wiki/Id3')) .'
'; $help .= t('You can:
'. t('For more information please read the configuration and customization handbook Audio page.', array('!audio' => 'http://www.drupal.org/handbook/modules/audio/')) .'
'; return $help; case 'admin/settings/audio': $help = ''. t('The current PHP configuration limits file uploads to %maxsize.', array('%maxsize' => format_size(file_upload_max_size()))) .'
';
$help .= '
'. t("There are two PHP ini settings, upload_max_filesize and post_max_size, that limit the maximum size of uploads. You can change these settings in the php.ini file or by using a php_value directive in Apache .htaccess file. Consult the PHP documentation for more info.") .'
'; return $help; case 'admin/settings/audio/metadata': $help = t("These settings let you determine what metadata the audio module tracks. You can add or remove metadata tags and select how they will be used.'. t('Note: deleting a tag will not remove it from the database or file until the node is saved again.') .'
'; return $help; } } /** * Implementation of hook_menu(). */ function audio_menu() { $items = array(); $items['admin/settings/audio'] = array( 'title' => 'Audio settings', 'description' => 'Change settings for the audio module.', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings'), 'access arguments' => array('administer site configuration'), 'file' => 'audio.admin.inc', ); $items['admin/settings/audio/main'] = array( 'title' => 'Audio', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => '-10', 'file' => 'audio.admin.inc', ); $items['admin/settings/audio/metadata'] = array( 'title' => 'Metadata tags', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings_metadata'), 'type' => MENU_LOCAL_TASK, 'file' => 'audio.admin.inc', ); $items['admin/settings/audio/players'] = array( 'title' => 'Players', 'page callback' => 'drupal_get_form', 'page arguments' => array('audio_admin_settings_players'), 'type' => MENU_LOCAL_TASK, 'file' => 'audio.admin.inc', ); $items['audio/autocomplete'] = array( 'page callback' => 'audio_autocomplete', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); $items['audio/by'] = array( 'title' => 'Browse by...', 'page callback' => 'audio_page_browse_by', 'access arguments' => array('access content'), 'type' => MENU_NORMAL_ITEM, ); $items['audio/download'] = array( 'page callback' => 'audio_download', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); $items['audio/play'] = array( 'page callback' => 'audio_play', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); return $items; } /** * Implementation of hook_theme */ function audio_theme() { return array( 'audio_file_form' => array( 'arguments' => array('form'), ), 'audio_admin_settings_metadata' => array( 'arguments' => array('form_element'), 'file' => 'audio.admin.inc', ), 'audio_admin_settings_players' => array( 'arguments' => array('form_element'), 'file' => 'audio.admin.inc', ), 'audio_default_node_player' => array( 'arguments' => array('node'), ), 'audio_teaser' => array( 'arguments' => array('node'), ), 'audio_display' => array( 'arguments' => array('node'), ), 'audio_format_tag' => array( 'arguments' => array('tag', 'value', 'setting'), ), 'audio_format_filelength' => array( 'arguments' => array('fileinfo'), ), 'audio_format_fileformat' => array( 'arguments' => array('fileinfo'), ), ); } /** * Implementation of hook_node_info(). */ function audio_node_info() { return array( 'audio' => array( 'name' => t('Audio'), 'module' => 'audio', 'description' => t('An audio file. The audio file could be used for adding music, podcasts, or audio clips to your site.'), ) ); } /** * Invoke hook_audio api methods. * * The audio module provide a hook for contributed modules. Use this rather than * the nodeapi so you don't have to worry about the module invocation order. * * function hook_audio($op, $node, $arg1) * * @param $op * The $op value will be one of the following: * 'load' * An audio node is being loaded. * 'prepare' * A node form/preview is being built. * 'submit' * A node form is being submitted, before insertion or updates. * 'insert' * A new audio node is being created. Changes you make to the node's * audio_images, audio_file, and audio_tags memebers will be saved. * 'insert revision' * A new revision of an audio node is being created. Changes you make to * the node's audio_images, and audio_file memebers will be saved. * 'update' * Changes to an audio node are being saved. Changes you make to the * node's audio_images, audio_file, and audio_tags memebers will be * saved. * 'delete' * An audio node is being deleted. Remove all of the nodes files and * database records. * 'delete revision' * A revision of the audio node is being deleted. Remove any version * specific files or database records. * 'upload' * A new audio file has been uploaded. Contrib modules can read data from * the file and append it to the audio node at this point. * 'access' * An operation is being performed on the node and other modules can * determine if this will be allowed. $arg1 will describe the operation * (i.e. 'play' or 'download'). Return a boolean if you want to allow or * deny this, or NULL if you don't. * 'download' * A user is playing the audio node. This can be handy if you're recording * user statistics. * 'play' * A user is downloading an audio node. This can be handy if you're * recording user statistics. * @param $node * An audio node object. */ function audio_invoke_audioapi($op, &$node, $a3 = NULL, $a4 = NULL) { $return = array(); foreach (module_implements('audio') as $name) { $function = $name .'_audio'; $result = $function($op, $node, $a3, $a4); if (isset($result) && is_array($result)) { $return = array_merge($return, $result); } else if (isset($result)) { $return[] = $result; } } return $return; } /** * Implementation of hook_perm(). */ function audio_perm() { return array('administer audio', 'create audio', 'edit own audio', 'play audio', 'download audio', 'view download stats'); } /** * Implementation of hook_access(). */ function audio_access($op, $node = NULL) { global $user; if (user_access('administer audio')) { return TRUE; } if ($op == 'update' || $op == 'delete') { if (($user->uid == $node->uid) && user_access('edit own audio')) { return TRUE; } } if ($op == 'create') { if (user_access('create audio')) { return TRUE; } } } /** * Is the current user allowed to download an audio node? * * @return * boolean indicating if it's allowed. */ function _audio_allow_download($node) { if (isset($node->url_download) && $node->audio_file['downloadable']) { $result = audio_invoke_audioapi('access', $node, 'download'); if (in_array(TRUE, $result)) { return TRUE; } if (in_array(FALSE, $result)) { return FALSE; } return user_access('download audio'); } return FALSE; } /** * Is the current user allowed to play an audio node? * * @return * boolean indicating if it's allowed. */ function _audio_allow_play($node) { if (isset($node->url_play)) { $result = audio_invoke_audioapi('access', $node, 'play'); if (in_array(TRUE, $result)) { return TRUE; } if (in_array(FALSE, $result)) { return FALSE; } return user_access('play audio'); } return FALSE; } /** * Implements hook_cron(). * * This deletes old temp files. */ function audio_cron() { $path = audio_get_directory() .'/temp'; $files = file_scan_directory(file_create_path($path), '.*'); foreach ($files as $file => $info) { if (time() - filemtime($file) > 60*60*6) { file_delete($file); } } } /** * Implementation of hook_link(). */ function audio_link($type, $node, $main = 0) { $links = array(); $link_access = user_access('view download stats'); if ($type == 'node' && $node->type == 'audio') { if (_audio_allow_download($node)) { $links['audio_download_link'] = array( 'title' => t('Download audio file'), 'href' => $node->url_download, ); if ($link_access) { $links['audio_download_count'] = array( 'title' => t('@download_count downloads', array('@download_count' => $node->audio_file['download_count'])), ); } } if (_audio_allow_play($node) && $link_access) { $links['audio_play_count'] = array( 'title' => t('@play_count plays', array('@play_count' => $node->audio_file['play_count'])), ); } } return $links; } /** * Implementation of hook_nodeapi(). */ function audio_nodeapi(&$node, $op, $arg) { if ($node->type == 'audio') { switch ($op) { case 'delete revision': audio_delete_revision($node); break; case 'rss item': $ret = array(); if (_audio_allow_download($node)) { // Reset the node's body to remove theming. $body = db_result(db_query("SELECT r.body FROM {node} n INNER JOIN {node_revisions} r ON n.nid = r.nid WHERE n.vid=%d", $node->vid)); $node->body = $body; $node = node_prepare($node, FALSE); $ret[] = array( 'key' => 'enclosure', 'attributes' => array( 'url' => $node->url_download, 'length' => $node->audio_file['file_size'], 'type' => $node->audio_file['file_mime'] )); // Provide very basic iTunes support. $ret[] = array( 'namespace' => array('itunes' => 'xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"'), ); $ret[] = array( 'key' => 'itunes:duration', 'value' => $node->audio_file['playtime'], ); $ret[] = array( 'key' => 'itunes:author', 'value' => $node->audio_tags['artist'], ); } return $ret; case 'update index': // Since the theme might hide the tag values we'll manually add them to // the search index. $cleantags = array(); foreach ($node->audio_tags as $value) { $cleantags[] = check_plain($value); } return $cleantags; } } } /** * Implementation of hook_view(). */ function audio_view(&$node, $teaser = FALSE, $page = FALSE) { drupal_add_css(drupal_get_path('module', 'audio') .'/audio.css'); $node = node_prepare($node, $teaser); if ($teaser) { $node->content['audio_info'] = array( '#value' => theme('audio_teaser', $node), '#weight' => 0, ); } else { $node->content['audio_info'] = array( '#value' => theme('audio_display', $node), '#weight' => -1, ); } return $node; } /** * Implementation of hook_validate(). */ function audio_validate(&$node, &$form) { $nid = ($node->nid) ? $node->nid : 'new_node'; if (!isset($node->audio_file['file_path']) && !isset($_SESSION['audio_new_file'][$nid])) { form_set_error('audio_upload', t("A file must be provided. If you tried uploading a file, make sure it's less than the upload size limit.")); } // Build the title from metadata. If there's not title format use the default. if (empty($node->title_format)) { $node->title_format = variable_get('audio_default_title_format', '[audio-tag-title-raw] by [audio-tag-artist-raw]'); } $node->title = token_replace($node->title_format, 'node', $node); $form_state['values']['title'] = $node->title; // FIXME } /** * Implementation of hook_load(). */ function audio_load($node) { if ($node->vid) { // This is a wonky way to load the fields but its handy right now while // I'm renaming fields in the databases. $fields = db_fetch_array(db_query("SELECT * FROM {audio} WHERE vid=%d", $node->vid)); $ret = array( 'title_format' => $fields['title_format'], 'audio_file' => array( 'play_count' => $fields['play_count'], 'download_count' => $fields['download_count'], 'downloadable' => $fields['downloadable'], 'file_format' => $fields['file_format'], 'file_mime' => $fields['file_mime'], 'file_name' => $fields['file_name'], 'file_path' => $fields['file_path'], 'file_size' => $fields['file_size'], 'sample_rate' => $fields['sample_rate'], 'channel_mode' => $fields['channel_mode'], 'bitrate' => $fields['bitrate'], 'bitrate_mode' => $fields['bitrate_mode'], 'playtime' => $fields['playtime'], 'bitrate' => $fields['bitrate'], ), ); if (file_exists($fields['file_path'])) { // TODO: should these links be by vid? $ret['url_play'] = url('audio/play/'. $node->nid, array('absolution' => TRUE)); if ($ret['audio_file']['downloadable']) { // iTunes and other podcasting programs check the url to determine the // file type. we'll add the original file name on to the end. see issues // #35398 and #68716 for more info. $url = 'audio/download/'. $node->nid .'/'. $fields['file_name']; $ret['url_download'] = url($url, array('absolute' => TRUE)); } } // Load the audio tags. $result = db_query("SELECT tag, value FROM {audio_metadata} WHERE vid=%d", $node->vid); $ret['audio_tags'] = array(); while ($obj = db_fetch_object($result)) { $ret['audio_tags'][$obj->tag] = $obj->value; } // Allow other modules to access newly loaded audio node. return array_merge($ret, audio_invoke_audioapi('load', $node)); } return array(); } /** * Implementation of hook_insert(). */ function audio_insert(&$node) { drupal_set_message('Hammy hoo...?'); file_move($node->audio_file['file_path'], audio_get_directory(), FILE_EXISTS_RENAME); // Notify other modules. audio_invoke_audioapi('insert', $node); $f = $node->audio_file; db_query("INSERT INTO {audio} (vid, nid, title_format, downloadable, file_format, file_name, file_path, file_mime, file_size, bitrate, bitrate_mode, sample_rate, channel_mode, playtime) VALUES (%d, %d, '%s', %d, '%s', '%s', '%s', '%s', %d, %f, '%s', %d, '%s', '%s')", $node->vid, $node->nid, $node->title_format, $f['downloadable'], $f['file_format'], $f['file_name'], $f['file_path'], $f['file_mime'], filesize($f['file_path']), $f['bitrate'], $f['bitrate_mode'], $f['sample_rate'], $f['channel_mode'], $f['playtime']); _audio_save_tags_to_db($node); // Unset the new file flag incase this node is re-saved. unset($node->audio_file['newfile']); } /** * Insert a new node revision. * * If a new file wasn't uploaded, make a copy of the existing file. */ function audio_insert_revision($node) { if (isset($node->audio_file['newfile'])) { file_move($node->audio_file['file_path'], audio_get_directory(), FILE_EXISTS_RENAME); } else { $newname = file_create_filename($node->audio_file['file_name'], audio_get_directory()); file_copy($node->audio_file['file_path'], $newname); } // Notify other modules. audio_invoke_audioapi('insert revision', $node); $f = $node->audio_file; db_query("INSERT INTO {audio} (vid, nid, title_format, downloadable, file_format, file_name, file_path, file_mime, file_size, bitrate, bitrate_mode, sample_rate, channel_mode, playtime) VALUES (%d, %d, '%s', %d, '%s', '%s', '%s', '%s', %d, %f, '%s', %d, '%s', '%s')", $node->vid, $node->nid, $node->title_format, $f['downloadable'], $f['file_format'], $f['file_name'], $f['file_path'], $f['file_mime'], filesize($f['file_path']), $f['bitrate'], $f['bitrate_mode'], $f['sample_rate'], $f['channel_mode'], $f['playtime']); _audio_save_tags_to_db($node); } /** * Implementation of hook_update(). */ function audio_update(&$node) { // Check for new revsion. if ($node->revision) { return audio_insert_revision($node); } if (isset($node->audio_file['newfile'])) { unset($node->audio_file['newfile']); // Remove the old file. $oldfile = db_fetch_object(db_query('SELECT file_path FROM {audio} f WHERE f.vid = %d', $node->vid)); file_delete($oldfile->file_path); // Save the new one. file_move($node->audio_file['file_path'], audio_get_directory(), FILE_EXISTS_RENAME); } // Notify other modules. audio_invoke_audioapi('update', $node); // Do a delete and insert rather than an update to ensure that we've got db // records. it takes a bit longer but it allows us to carry on if one of the // records wasn't created by audio_insert(). db_query("DELETE FROM {audio} WHERE vid=%d", $node->vid); $f = $node->audio_file; db_query("INSERT INTO {audio} (vid, nid, title_format, play_count, download_count, downloadable, file_format, file_name, file_path, file_mime, file_size, bitrate, bitrate_mode, sample_rate, channel_mode, playtime) VALUES (%d, %d, '%s', %d, %d, %d, '%s', '%s', '%s', '%s', %d, %f, '%s', %d, '%s', '%s')", $node->vid, $node->nid, $node->title_format, $f['play_count'], $f['download_count'], $f['downloadable'], $f['file_format'], $f['file_name'], $f['file_path'], $f['file_mime'], filesize($f['file_path']), $f['bitrate'], $f['bitrate_mode'], $f['sample_rate'], $f['channel_mode'], $f['playtime']); _audio_save_tags_to_db($node); } /** * Implementation of hook_delete(). */ function audio_delete($node) { // Notify other modules. audio_invoke_audioapi('delete', $node); $result = db_query('SELECT vid, file_path FROM {audio} WHERE nid = %d', $node->nid); while ($o = db_fetch_object($result)) { if (isset($o->file_path)) { file_delete($o->file_path); } db_query('DELETE FROM {audio_metadata} WHERE vid = %d', $o->vid); } db_query('DELETE FROM {audio} WHERE nid = %d', $node->nid); } /** * Delete a single revision. */ function audio_delete_revision($node) { // Notify other modules. audio_invoke_audioapi('delete revision', $node); if ($filepath = db_result(db_query('SELECT file_path FROM {audio} WHERE vid = %d', $node->vid))) { file_delete($filepath); } db_query('DELETE FROM {audio_metadata} WHERE vid = %d', $node->vid); db_query('DELETE FROM {audio} WHERE vid = %d', $node->vid); } /** * Implementation of hook_prepare(). * * We do a lot here because we need to read the file's meta data after upload * but before both the form and node preview are built. */ function audio_prepare(&$node) { // We need to be aware that a user may try to edit multiple audio nodes at // once. By using the $nid variable each node's files can be stored separately // in the session. $nid = empty($node->nid) ? 'new_node' : $node->nid; // When you enter the edit view the first time we need to clear our files in // session for this node. This is so if you upload a file, then decide you // don't want it and reload the form (without posting), the files will be // discarded. if (count($_POST) == 0) { unset($_SESSION['audio_new_file'][$nid]); } // Check for an upload. $validators = array( 'file_validate_extensions' => variable_get('audio_allowed_extensions', 'mp3 wav ogg'), ); if ($file = file_save_upload('audio_upload', $validators)) { file_copy($file, 'files/audio/'. $file->filename, FILE_EXISTS_REPLACE); $node->audio_file = array( 'newfile' => TRUE, 'play_count' => 0, 'download_count' => 0, 'downloadable' => (bool) $_POST['audio_file']['downloadable'], 'file_name' => $file->filename, 'file_path' => $file->filepath, 'file_mime' => $file->filemime, 'file_size' => filesize($node->audio_file['file_path']), ); // Allow other modules to modify the node. audio_invoke_audioapi('upload', $node); // ...save info to $_POST so that it shows up in both the preview and // form. Note that we do this after calling we called our api hook with // the upload operation, it gives the audio_id3 module a chance to read // the tags. $_POST['audio_file'] = $node->audio_file; $_POST['audio_tags'] = $node->audio_tags; $_SESSION['audio_new_file'][$nid] = $node->audio_file['file_path']; } // Assign a newly uploaded file that's being previewed. else if (isset($_SESSION['audio_new_file'][$nid])) { $node->audio_file['file_path'] = $_SESSION['audio_new_file'][$nid]; } audio_invoke_audioapi('prepare', $node); } /** * Implementation of hook_form(). */ function audio_form(&$node) { $type = node_get_types('type', $node); if ($type->has_title) { $form['title']['#weight'] = -5; $form['title']['title_format'] = array( '#type' => 'textfield', '#title' => check_plain($type->title_label), '#default_value' => !empty($node->title_format) ? $node->title_format : variable_get('audio_default_title_format', '[audio-tag-title-raw] by [audio-tag-artist-raw]'), '#description' => t("The title can use the file's metadata. You can use the tokens listed below to insert information into the title. Note: the node title is escaped so it is safe to use the -raw tokens."), '#required' => TRUE, ); $form['title']['token_help'] = array( '#title' => t('Token list'), '#type' => 'fieldset', '#collapsible' => TRUE, '#collapsed' => TRUE, '#description' => t('This is a list of the tokens that can be used in the title of audio nodes.'), 'help' => array('#value' => theme('token_help', 'node')), ); } if ($type->has_body) { $form['body_filter']['#weight'] = -4; $form['body_filter']['body'] = array( '#type' => 'textarea', '#title' => check_plain($type->body_label), '#default_value' => $node->body, '#rows' => 5, '#required' => ($type->min_word_count > 0), ); $form['body_filter']['format'] = filter_form($node->format); } $form['#attributes'] = array('enctype' => 'multipart/form-data'); if (empty($node->nid)) { $node->audio_file = array( 'file_path' => '', 'file_name' => '', 'file_mime' => '', ); } $form['audio_file'] = array( '#type' => 'fieldset', '#title' => t('Audio File Info'), '#collapsible' => TRUE, '#weight' => -1, '#tree' => TRUE, ); // Store the non-user editable file information as values. $form['audio_file']['file_path'] = array( '#type' => 'value', '#value' => $node->audio_file['file_path'], ); $form['audio_file']['file_name'] = array( '#type' => 'value', '#value' => $node->audio_file['file_name'], ); $form['audio_file']['file_mime'] = array( '#type' => 'value', '#value' => $node->audio_file['file_mime'], ); // Place a visible copy of the file path on the form (after removing the // directory info from non-admins). $file_path = $node->audio_file['file_path']; if (!user_access('administer audio')) { $file_path = basename($file_path); } $form['audio_file']['display_file_path'] = array( '#type' => 'item', '#title' => t('Current File'), '#value' => !empty($file_path) ? $file_path : t('No file is attached.'), ); // If we've got a file, add the file information fields. if (!empty($node->audio_file['file_name'])) { $form['audio_file']['#theme'] = 'audio_file_form'; $form['audio_file']['file_format'] = array( '#type' => 'select', '#title' => t('Format'), '#default_value' => $node->audio_file['file_format'], '#options' => drupal_map_assoc(array('', 'aac', 'ac3', 'au', 'avr', 'flac', 'midi', 'mod', 'mp3', 'mpc', 'ogg', 'voc'), 'drupal_strtoupper'), ); $form['audio_file']['file_size'] = array( '#type' => 'textfield', '#title' => t('File Size'), '#default_value' => $node->audio_file['file_size'], ); $form['audio_file']['playtime'] = array( '#type' => 'textfield', '#title' => t('Length'), '#default_value' => $node->audio_file['playtime'], '#description' => t('The format is hours:minutes:seconds.'), ); $form['audio_file']['sample_rate'] = array( '#type' => 'select', '#title' => t('Sample rate'), '#default_value' => $node->audio_file['sample_rate'], '#options' => array('' => '', '48000' => '48,000 Hz', '44100' => '44,100 Hz', '32000' => '32,000 Hz', '22050' => '22,050 Hz', '11025' => '11,025 Hz', '8000' => '8,000 Hz'), ); $form['audio_file']['channel_mode'] = array( '#type' => 'select', '#title' => t('Channel mode'), '#default_value' => $node->audio_file['channel_mode'], '#options' => array('stereo' => t('Stereo'), 'mono' => t('Mono')), ); $form['audio_file']['bitrate'] = array( '#type' => 'textfield', '#title' => t('Bitrate'), '#default_value' => $node->audio_file['bitrate'], ); $form['audio_file']['bitrate_mode'] = array( '#type' => 'select', '#title' => t('Bitrate mode'), '#default_value' => $node->audio_file['bitrate_mode'], '#options' => array('' => '', 'cbr' => t('Constant'), 'vbr' => t('Variable')), ); // Users shouldn't be able to change the play and download counts so we'll // put these for viewing... $form['audio_file']['display_play_count'] = array( '#type' => 'item', '#title' => t('Play count'), '#value' => $node->audio_file['play_count'], ); $form['audio_file']['display_download_count'] = array( '#type' => 'item', '#title' => t('Download count'), '#value' => $node->audio_file['download_count'], ); // ...and these are what we'll save back to the node. $form['audio_file']['play_count'] = array( '#type' => 'value', '#value' => $node->audio_file['play_count'], ); $form['audio_file']['download_count'] = array( '#type' => 'value', '#value' => $node->audio_file['download_count'], ); } $form['audio_file']['audio_upload'] = array( '#tree' => FALSE, '#type' => 'file', '#title' => empty($node->audio_file['file_name']) ? t('Add a new audio file') : t('Replace this with a new file'), '#description' => t('Click "Browse..." to select an audio file to upload. Only files with the following extensions are allowed: %allowed-extensions.', array('%allowed-extensions' => variable_get('audio_allowed_extensions', 'mp3 wav ogg'))) .'