/********************************************************************** Audacity: A Digital Audio Editor ExportMultiple.cpp Dominic Mazzoni *******************************************************************//** \class ExportMultiple \brief Presents a dialog box allowing the user to export multiple files either by exporting each track as a separate file, or by exporting each label as a separate file. *//********************************************************************/ #include "../Audacity.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Export.h" #include "ExportMultiple.h" #include "../Internat.h" #include "../FileFormats.h" #include "../FileNames.h" #include "../LabelTrack.h" #include "../Project.h" #include "../Prefs.h" #include "../Tags.h" #include "../widgets/ErrorDialog.h" /* define our dynamic array of export settings */ #include // much hackery WX_DEFINE_OBJARRAY( ExportKitArray ) enum { FormatID = 10001, OptionsID, DirID, CreateID, ChooseID, LabelID, FirstID, FirstFileNameID, TrackID, ByNameAndNumberID, ByNameID, ByNumberID, PrefixID, OverwriteID }; // // ExportMultiple methods // BEGIN_EVENT_TABLE(ExportMultiple, wxDialog) EVT_CHOICE(FormatID, ExportMultiple::OnFormat) EVT_BUTTON(OptionsID, ExportMultiple::OnOptions) EVT_BUTTON(CreateID, ExportMultiple::OnCreate) EVT_BUTTON(ChooseID, ExportMultiple::OnChoose) EVT_BUTTON(wxID_OK, ExportMultiple::OnExport) EVT_BUTTON(wxID_CANCEL, ExportMultiple::OnCancel) EVT_RADIOBUTTON(LabelID, ExportMultiple::OnLabel) EVT_RADIOBUTTON(TrackID, ExportMultiple::OnTrack) EVT_RADIOBUTTON(ByNameAndNumberID, ExportMultiple::OnByName) EVT_RADIOBUTTON(ByNameID, ExportMultiple::OnByName) EVT_RADIOBUTTON(ByNumberID, ExportMultiple::OnByNumber) EVT_CHECKBOX(FirstID, ExportMultiple::OnFirst) EVT_TEXT(FirstFileNameID, ExportMultiple::OnFirstFileName) EVT_TEXT(PrefixID, ExportMultiple::OnPrefix) END_EVENT_TABLE() BEGIN_EVENT_TABLE(SuccessDialog, wxDialog) EVT_LIST_KEY_DOWN(wxID_ANY, SuccessDialog::OnKeyDown) EVT_LIST_ITEM_ACTIVATED(wxID_ANY, SuccessDialog::OnItemActivated) // happens when is pressed with list item having focus END_EVENT_TABLE() BEGIN_EVENT_TABLE(MouseEvtHandler, wxEvtHandler) EVT_LEFT_DCLICK(MouseEvtHandler::OnMouse) END_EVENT_TABLE() ExportMultiple::ExportMultiple(AudacityProject *project) : wxDialog(project, wxID_ANY, wxString(_("Export Multiple"))) { mProject = project; mTracks = project->GetTracks(); mPlugins = mExporter.GetPlugins(); this->CountTracksAndLabels(); // create array of characters not allowed in file names wxString forbid = wxFileName::GetForbiddenChars(); for(unsigned int i=0; i < forbid.Length(); i++) exclude.Add( forbid.Mid(i, 1) ); ShuttleGui S(this, eIsCreatingFromPrefs); // Creating some of the widgets cause cause events to fire // and we don't want that until after we're completely // created. (Observed on Windows) mInitialized = false; PopulateOrExchange(S); mInitialized = true; Layout(); Fit(); SetMinSize(GetSize()); Center(); EnableControls(); } ExportMultiple::~ExportMultiple() { } void ExportMultiple::CountTracksAndLabels() { mLabels = NULL; mNumLabels = 0; mNumWaveTracks = 0; Track* pTrack; for (pTrack = mIterator.First(mTracks); pTrack != NULL; pTrack = mIterator.Next()) { switch (pTrack->GetKind()) { // Count WaveTracks, and for linked pairs, count only the second of the pair. case Track::Wave: { if (pTrack->GetLinked() == false) mNumWaveTracks++; break; } // Only support one label track??? case Track::Label: { // Supports only one LabelTrack. if (mLabels == NULL) { mLabels = (LabelTrack*)pTrack; mNumLabels = mLabels->GetNumLabels(); } break; } } } } int ExportMultiple::ShowModal() { if (mNumWaveTracks < 2 && mNumLabels < 1) { ::wxMessageBox(_("If you have more than one Audio Track, you can export each track as a separate file,\nor if you have a Label Track, you can export a new file for each label.\n\nThis project does not have multiple tracks or a Label Track, so you cannot export multiple files."), _("Can't export multiple files"), wxOK | wxCENTRE, this); return wxID_CANCEL; } if (mNumLabels < 1) { mLabel->Enable(false); mTrack->SetValue(true); mLabel->SetValue(false); } if (mNumWaveTracks < 2) { mTrack->Enable(false); mLabel->SetValue(true); mTrack->SetValue(false); } EnableControls(); return wxDialog::ShowModal(); } void ExportMultiple::PopulateOrExchange(ShuttleGui& S) { wxString name = mProject->GetName(); mPluginIndex = -1; wxString defaultFormat = gPrefs->Read(wxT("/Export/Format"), wxT("WAV")); mFilterIndex = 0; for (size_t i = 0; i < mPlugins.GetCount(); i++) { for (int j = 0; j < mPlugins[i]->GetFormatCount(); j++) { if (mPlugins[i]->GetFormat(j) == defaultFormat) { mPluginIndex = i; mSubFormatIndex = j; } if (mPluginIndex == -1) mFilterIndex++; } } if (mPluginIndex == -1) { mPluginIndex = 0; mFilterIndex = 0; mSubFormatIndex = 0; } S.SetBorder(5); S.StartMultiColumn(4, true); { wxArrayString formats; for (size_t i = 0; i < mPlugins.GetCount(); i++) { for (int j = 0; j < mPlugins[i]->GetFormatCount(); j++) { formats.Add(mPlugins[i]->GetDescription(j)); } } mFormat = S.Id(FormatID) .TieChoice(_("Export format:"), wxT("/Export/MultipleFormat"), mPlugins[mPluginIndex]->GetFormat(mSubFormatIndex), formats, formats); S.Id(OptionsID).AddButton(_("Options...")); S.AddVariableText(wxT(""), false); mDir = S.Id(DirID) .TieTextBox(_("Export location:"), wxT("/Export/MultiplePath"), gPrefs->Read(wxT("/Export/Path"), ::wxGetCwd()), 64); S.Id(ChooseID).AddButton(_("Choose...")); S.Id(CreateID).AddButton(_("Create")); } S.EndMultiColumn(); S.StartHorizontalLay(wxEXPAND, true); { S.SetBorder(5); S.StartStatic(_("Split files based on:"), true); { S.StartMultiColumn(2, false); S.SetStretchyCol(1); { // Row 1 S.SetBorder(1); mLabel = S.Id(LabelID) .AddRadioButton(wxT("")); mLabel->SetName(_("Labels")); S.SetBorder(3); mLabelLabel = S.AddVariableText(_("Labels"), false); // Row 2 S.AddVariableText(wxT(""), false); mFirst = S.Id(FirstID) .AddCheckBox(_("Include audio before first label"), wxT("false")); // Row 3 S.AddVariableText(wxT(""), false); S.StartHorizontalLay(wxEXPAND, false); { mFirstFileLabel = S.AddVariableText(_("First file name:"), true); mFirstFileName = S.Id(FirstFileNameID) .TieTextBox(wxT(""), name, 30); mFirstFileName->SetName(_("First file name")); } S.EndHorizontalLay(); // Row 4 S.SetBorder(1); mTrack = S.Id(TrackID) .AddRadioButtonToGroup(wxT("")); mTrack->SetName(_("Tracks")); S.SetBorder(3); mTrackLabel = S.AddVariableText(_("Tracks"), false); } S.EndMultiColumn(); } S.EndStatic(); S.SetBorder(5); S.StartStatic(_("Name files:"), false); { S.SetBorder(2); S.StartRadioButtonGroup(wxT("/Export/TrackNameWithOrWithoutNumbers"), wxT("labelTrack")); { mByName = S.Id(ByNameID) .TieRadioButton(_("Using Label/Track Name"), wxT("labelTrack")); mByNumberAndName = S.Id(ByNameAndNumberID) .TieRadioButton(_("Numbering before Label/Track Name"), wxT("numberBefore")); mByNumber = S.Id(ByNumberID) .TieRadioButton(_("Numbering after Label/Track Name"), wxT("numberAfter")); } S.EndRadioButtonGroup(); S.StartHorizontalLay(wxEXPAND, false); { mPrefixLabel = S.AddVariableText(_("File name prefix:"), true); mPrefix = S.Id(PrefixID) .TieTextBox(wxT(""), name, 30); mPrefix->SetName(_("File name prefix")); } S.EndHorizontalLay(); } S.EndStatic(); } S.EndHorizontalLay(); S.SetBorder(5); S.StartHorizontalLay(wxEXPAND, false); { mOverwrite = S.Id(OverwriteID) .AddCheckBox(_("Overwrite existing files"), wxT("true")); } S.EndHorizontalLay(); S.AddStandardButtons(); mExport = (wxButton *)wxWindow::FindWindowById(wxID_OK, this); mExport->SetLabel(_("Export")); } void ExportMultiple::EnableControls() { bool enable; if (!mInitialized) { return; } mFirst->Enable(mLabel->GetValue()); enable = mLabel->GetValue() && (mByName->GetValue() || mByNumberAndName->GetValue()) && mFirst->GetValue(); mFirstFileLabel->Enable(enable); mFirstFileName->Enable(enable); enable = mByNumber->GetValue(); mPrefixLabel->Enable(enable); mPrefix->Enable(enable); bool ok = true; if (mLabel->GetValue() && mFirst->GetValue() && mFirstFileName->GetValue() == wxT("") && mPrefix->GetValue() == wxT("")) ok = false; if (mByNumber->GetValue() && mPrefix->GetValue() == wxT("")) ok = false; mExport->Enable(ok); } void ExportMultiple::OnFormat(wxCommandEvent& event) { EnableControls(); } void ExportMultiple::OnOptions(wxCommandEvent& event) { const int sel = mFormat->GetSelection(); if (sel != wxNOT_FOUND) { size_t c = 0; for (size_t i = 0; i < mPlugins.GetCount(); i++) { for (int j = 0; j < mPlugins[i]->GetFormatCount(); j++) { if ((size_t)sel == c) { mPluginIndex = i; mSubFormatIndex = j; } c++; } } } mPlugins[mPluginIndex]->DisplayOptions(this,mSubFormatIndex); } void ExportMultiple::OnCreate(wxCommandEvent& event) { wxFileName fn; fn.AssignDir(mDir->GetValue()); bool ok = fn.Mkdir(0777, wxPATH_MKDIR_FULL); if (!ok) { // Mkdir will produce an error dialog return; } ::wxMessageBox(wxString::Format(_("\"%s\" successfully created."), fn.GetPath().c_str()), _("Export Multiple"), wxOK | wxCENTRE, this); } void ExportMultiple::OnChoose(wxCommandEvent& event) { wxDirDialog dlog(this, _("Choose a location to save the exported files"), mDir->GetValue()); dlog.ShowModal(); if (dlog.GetPath() != wxT("")) mDir->SetValue(dlog.GetPath()); } void ExportMultiple::OnLabel(wxCommandEvent& event) { EnableControls(); } void ExportMultiple::OnFirst(wxCommandEvent& event) { EnableControls(); } void ExportMultiple::OnFirstFileName(wxCommandEvent& event) { EnableControls(); } void ExportMultiple::OnTrack(wxCommandEvent& event) { EnableControls(); } void ExportMultiple::OnByName(wxCommandEvent& event) { EnableControls(); } void ExportMultiple::OnByNumber(wxCommandEvent& event) { EnableControls(); } void ExportMultiple::OnPrefix(wxCommandEvent& event) { EnableControls(); } void ExportMultiple::OnCancel(wxCommandEvent& event) { EndModal(0); } void ExportMultiple::OnExport(wxCommandEvent& event) { ShuttleGui S(this, eIsSavingToPrefs); PopulateOrExchange(S); // Make sure the output directory is in good shape if (!DirOk()) { return; } mFilterIndex = mFormat->GetSelection(); if (mFilterIndex != wxNOT_FOUND) { size_t c = 0; for (size_t i = 0; i < mPlugins.GetCount(); i++) { for (int j = 0; j < mPlugins[i]->GetFormatCount(); j++) { if ((size_t)mFilterIndex == c) { // this is the selected format. Store the plug-in and sub-format // needed to acheive it. mPluginIndex = i; mSubFormatIndex = j; } c++; } } } // bool overwrite = mOverwrite->GetValue(); int ok; mExported.Empty(); if (mLabel->GetValue()) { ok = ExportMultipleByLabel(mByName->GetValue() || mByNumberAndName->GetValue(), mPrefix->GetValue(), mByNumberAndName->GetValue()); } else { ok = ExportMultipleByTrack(mByName->GetValue() || mByNumberAndName->GetValue(), mPrefix->GetValue(), mByNumberAndName->GetValue()); } // Give 'em the result { wxString msg; msg.Printf( ok == eProgressSuccess ? _("Successfully exported the following %ld file(s).") : (ok == eProgressFailed ? _("Something went wrong after exporting the following %ld file(s).") : (ok == eProgressCancelled ? _("Export canceled after exporting the following %ld file(s).") : (ok == eProgressStopped ? _("Export stopped after exporting the following %ld file(s).") : _("Something went really wrong after exporting the following %ld file(s).") ) ) ), mExported.GetCount()); wxString FileList; for (size_t i = 0; i < mExported.GetCount(); i++) { FileList += mExported[i]; FileList += '\n'; } // This results dialog is a child of this dialog. ShowInfoDialog( this, _("Export Multiple"), msg, FileList, 450,400); } if (ok == eProgressSuccess || ok == eProgressStopped) { EndModal(1); } } bool ExportMultiple::DirOk() { wxFileName fn; fn.AssignDir(mDir->GetValue()); if (fn.DirExists()) { return true; } wxString prompt; prompt.Printf(_("\"%s\" doesn't exist.\n\nWould you like to create it?"), fn.GetFullPath().c_str()); int action = wxMessageBox(prompt, wxT("Warning"), wxYES_NO | wxICON_EXCLAMATION); if (action != wxYES) { return false; } return fn.Mkdir(0777, wxPATH_MKDIR_FULL); } int ExportMultiple::ExportMultipleByLabel(bool byName, wxString prefix, bool addNumber) { wxASSERT(mProject); bool tagsPrompt = mProject->GetShowId3Dialog(); int numFiles = mNumLabels; int l = 0; // counter for files done ExportKitArray exportSettings; // dynamic array we will use to store the // settings needed to do the exports with in exportSettings.Alloc(numFiles); // allocated some guessed space to use // Account for exporting before first label if (mFirst->GetValue()) { l--; numFiles++; } // Figure out how many channels we should export int channels = (mTracks->GetNumExportChannels(false)); wxArrayString otherNames; // keep track of file names we will use, so we // don't duplicate them ExportKit setting; // the current batch of settings setting.destfile.SetPath(mDir->GetValue()); setting.destfile.SetExt(mPlugins[mPluginIndex]->GetExtension(mSubFormatIndex)); wxLogDebug(wxT("Plug-in index = %d, Sub-format = %d"), mPluginIndex, mSubFormatIndex); wxLogDebug(wxT("File extension is %s"), setting.destfile.GetExt().c_str()); wxString name; // used to hold file name whilst we mess with it wxString title; // un-messed-with title of file for tagging with const LabelStruct *info = NULL; /* Examine all labels a first time, sort out all data but don't do any * exporting yet (so this run is quick but interactive) */ while (l < mNumLabels) { // Get file name and starting time if (l < 0) { // create wxFileName for output file name = (mFirstFileName->GetValue()); setting.t0 = 0.0; } else { info = mLabels->GetLabel(l); name = (info->title); setting.t0 = info->t; } // Figure out the ending time if (info && info->t < info->t1) { setting.t1 = info->t1; } else if (l < mNumLabels-1) { const LabelStruct *info1 = mLabels->GetLabel(l+1); setting.t1 = info1->t; } else { setting.t1 = mTracks->GetEndTime(); } if( name.IsEmpty() ) name = _("untitled"); // store title of label to use in tags title = name; // Numbering files... if (!byName) { name.Printf(wxT("%s-%02d"), prefix.c_str(), l+1); } else if (addNumber) { // Following discussion with GA, always have 2 digits // for easy file-name sorting (on Windows) name.Prepend(wxString::Format(wxT("%02d-"), l+1)); } // store sanitised and user checked name in object setting.destfile.SetName(MakeFileName(name)); wxASSERT(setting.destfile.IsOk()); // scream if file name is broke // Make sure the (final) file name is unique within the set of exports FileNames::MakeNameUnique(otherNames, setting.destfile); /* do the metadata for this file */ // copy project metadata to start with setting.filetags = *(mProject->GetTags()); // over-ride with values setting.filetags.SetTag(TAG_TITLE, title); setting.filetags.SetTag(TAG_TRACK, l+1); // let the user have a crack at editing it, exit if cancelled if (!setting.filetags.ShowEditDialog(mProject,_("Edit metadata"), tagsPrompt)) return false; /* add the settings to the array of settings to be used for export */ exportSettings.Add(setting); l++; // next label, count up one } int ok = eProgressSuccess; // did it work? int count = 0; // count the number of sucessful runs ExportKit activeSetting; // pointer to the settings in use for this export /* Go round again and do the exporting (so this run is slow but * non-interactive) */ for (count = 0; count < numFiles; count++) { /* get the settings to use for the export from the array */ activeSetting = exportSettings[count]; // Export it ok = DoExport(channels, activeSetting.destfile, false, activeSetting.t0, activeSetting.t1, activeSetting.filetags); if (ok != eProgressSuccess && ok != eProgressStopped) { break; } } return ok; } int ExportMultiple::ExportMultipleByTrack(bool byName, wxString prefix, bool addNumber) { wxASSERT(mProject); bool tagsPrompt = mProject->GetShowId3Dialog(); Track *tr, *tr2; int channels = 0; // how many channels export? int l = 0; // track counter int numTracks = 0; int ok = eProgressSuccess; wxArrayString otherNames; wxArrayPtrVoid selected; /**< Array of pointers to the tracks which were selected when we started */ ExportKitArray exportSettings; // dynamic array we will use to store the // settings needed to do the exports with in exportSettings.Alloc(mNumLabels); // allocated some guessed space to use ExportKit setting; // the current batch of settings setting.destfile.SetPath(mDir->GetValue()); setting.destfile.SetExt(mPlugins[mPluginIndex]->GetExtension(mSubFormatIndex)); wxString name; // used to hold file name whilst we mess with it wxString title; // un-messed-with title of file for tagging with /* Remember which tracks were selected, and set them to unselected */ for (tr = mIterator.First(mTracks); tr != NULL; tr = mIterator.Next()) { if (tr->GetKind() != Track::Wave) { continue; } if (tr->GetSelected()) { selected.Add(tr); tr->SetSelected(false); } if (!tr->GetLinked()) { numTracks++; } } /* Examine all tracks in turn, collecting export information */ for (tr = mIterator.First(mTracks); tr != NULL; tr = mIterator.Next()) { // Only want wave tracks if (tr->GetKind() != Track::Wave) { continue; } // Get the times for the track setting.t0 = tr->GetStartTime(); setting.t1 = tr->GetEndTime(); // Check for a linked track tr2 = NULL; if (tr->GetLinked()) { tr2 = mIterator.Next(); if (tr2) { // Make sure it gets included if (tr2->GetStartTime() < setting.t0) { setting.t0 = tr2->GetStartTime(); } if (tr2->GetEndTime() > setting.t1) { setting.t1 = tr2->GetEndTime(); } } } // number of export channels? if (tr2 == NULL && tr->GetChannel() == WaveTrack::MonoChannel && ((WaveTrack *)tr)->GetPan() == 0.0) channels = 1; else channels = 2; // Get name and title title = tr->GetName(); if( title.IsEmpty() ) title = _("untitled"); if (byName) { name = title; if (addNumber) { name.Prepend(wxString::Format(wxT("%02d-"), l+1)); } } else { name = (wxString::Format(wxT("%s-%02d"), prefix.c_str(), l+1)); } // store sanitised and user checked name in object setting.destfile.SetName(MakeFileName(name)); if (setting.destfile.GetName().IsEmpty()) { // user cancelled dialogue, or deleted everything in feild. // either way, cancel return false; } wxASSERT(setting.destfile.IsOk()); // scream if file name is broke // Make sure the (final) file name is unique within the set of exports FileNames::MakeNameUnique(otherNames, setting.destfile); /* do the metadata for this file */ // copy project metadata to start with setting.filetags = *(mProject->GetTags()); // over-ride with values setting.filetags.SetTag(TAG_TITLE, title); setting.filetags.SetTag(TAG_TRACK, l+1); // let the user have a crack at editing it, exit if cancelled if (!setting.filetags.ShowEditDialog(mProject,_("Edit metadata"), tagsPrompt)) return false; /* add the settings to the array of settings to be used for export */ exportSettings.Add(setting); l++; // next track, count up one } // end of user-interactive data gathering loop, start of export processing // loop int count = 0; // count the number of sucessful runs ExportKit activeSetting; // pointer to the settings in use for this export for (tr = mIterator.First(mTracks); tr != NULL; tr = mIterator.Next()) { // Only want wave tracks if (tr->GetKind() != Track::Wave) { continue; } /* Select the track */ tr->SetSelected(true); // Check for a linked track tr2 = NULL; if (tr->GetLinked()) { tr2 = mIterator.Next(); if (tr2) { // Select it also tr2->SetSelected(true); } } /* get the settings to use for the export from the array */ activeSetting = exportSettings[count]; // export data ok = DoExport(channels, activeSetting.destfile, true, activeSetting.t0, activeSetting.t1, activeSetting.filetags); // Reset selection state tr->SetSelected(false); if (tr2) { tr2->SetSelected(false); } // Stop if an error occurred if (ok != eProgressSuccess && ok != eProgressStopped) { break; } // increment export counter count++; } // Restore the selection states for (size_t i = 0; i < mSelected.GetCount(); i++) { ((Track *) selected[i])->SetSelected(true); } return ok; } int ExportMultiple::DoExport(int channels, wxFileName name, bool selectedOnly, double t0, double t1, Tags tags) { wxLogDebug(wxT("Doing multiple Export: File name \"%s\""), (name.GetFullName()).c_str()); wxLogDebug(wxT("Channels: %i, Start: %lf, End: %lf "), channels, t0, t1); if (selectedOnly) wxLogDebug(wxT("Selected Region Only")); else wxLogDebug(wxT("Whole Project")); if (mOverwrite->GetValue()) { // Make sure we don't overwrite (corrupt) alias files if (!mProject->GetDirManager()->EnsureSafeFilename(name)) { return false; } } else { int i = 2; wxString base(name.GetName()); while (name.FileExists()) { name.SetName(wxString::Format(wxT("%s-%d"), base.c_str(), i++)); } } // Call the format export routine int success = mPlugins[mPluginIndex]->Export(mProject, channels, name.GetFullPath(), selectedOnly, t0, t1, NULL, &tags, mSubFormatIndex); if (success == eProgressSuccess || success == eProgressStopped) { mExported.Add(name.GetFullPath()); } return success; } wxString ExportMultiple::MakeFileName(wxString input) { wxString newname; // name we are generating // strip out anything that isn't allowed in file names on this platform newname = Internat::SanitiseFilename(input, wxT("_")); if(!newname.IsSameAs(input)) { // need to get user to fix file name // build the dialog wxString msg; msg.Printf(_("Label %s is not a legal file name. You cannot use any of: %s\nUse..."), input.c_str(), wxFileName::GetForbiddenChars().c_str()); wxTextEntryDialog dlg( this, msg, _("Save As..."), newname ); // And tell the validator about excluded chars dlg.SetTextValidator( wxFILTER_EXCLUDE_CHAR_LIST ); wxTextValidator *tv = dlg.GetTextValidator(); tv->SetExcludes(exclude); // Show the dialog and bail if the user cancels if( dlg.ShowModal() == wxID_CANCEL ) { return wxEmptyString; } // Extract the name from the dialog newname = dlg.GetValue(); } // phew - end of file name sanitisation procedure return newname; } void SuccessDialog::OnKeyDown(wxListEvent& event) { if (event.GetKeyCode() == WXK_RETURN) EndModal(1); else event.Skip(); // allow standard behaviour } void SuccessDialog::OnItemActivated(wxListEvent& event) { EndModal(1); } void MouseEvtHandler::OnMouse(wxMouseEvent& event) { event.Skip(false); } // Indentation settings for Vim and Emacs and unique identifier for Arch, a // version control system. Please do not modify past this point. // // Local Variables: // c-basic-offset: 3 // indent-tabs-mode: nil // End: // // vim: et sts=3 sw=3 // arch-tag: d6904b91-a320-4194-8d60-caa9175b6bb4