changeset 161:4b19d824a213 jams

Merge from default branch
author Chris Cannam
date Wed, 15 Oct 2014 11:33:14 +0100
parents ad96fd5f9cd7 (diff) 2a399bed0184 (current diff)
children 447230267c0d
files runner.pro runner/FeatureWriterFactory.cpp runner/JAMSFeatureWriter.cpp tests/test.sh
diffstat 10 files changed, 630 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/runner.pro	Wed Oct 15 11:23:05 2014 +0100
+++ b/runner.pro	Wed Oct 15 11:33:14 2014 +0100
@@ -84,6 +84,7 @@
         runner/FeatureWriterFactory.h  \
         runner/DefaultFeatureWriter.h \
         runner/FeatureExtractionManager.h \
+        runner/JAMSFeatureWriter.h \
         runner/LabFeatureWriter.h \
         runner/MIDIFeatureWriter.h \
         runner/MultiplexedReader.h
@@ -94,6 +95,7 @@
 	runner/FeatureExtractionManager.cpp \
         runner/AudioDBFeatureWriter.cpp \
         runner/FeatureWriterFactory.cpp \
+        runner/JAMSFeatureWriter.cpp \
         runner/LabFeatureWriter.cpp \
         runner/MIDIFeatureWriter.cpp \
         runner/MultiplexedReader.cpp
--- a/runner/FeatureWriterFactory.cpp	Wed Oct 15 11:23:05 2014 +0100
+++ b/runner/FeatureWriterFactory.cpp	Wed Oct 15 11:33:14 2014 +0100
@@ -23,6 +23,7 @@
 
 #include "AudioDBFeatureWriter.h"
 #include "MIDIFeatureWriter.h"
+#include "JAMSFeatureWriter.h"
 #include "LabFeatureWriter.h"
 
 set<string>
@@ -35,6 +36,7 @@
     tags.insert("csv");
     tags.insert("lab");
     tags.insert("midi");
+    tags.insert("json");
     return tags;
 }
 
@@ -53,6 +55,8 @@
         return new LabFeatureWriter();
     } else if (tag == "midi") {
         return new MIDIFeatureWriter();
+    } else if (tag == "json") {
+        return new JAMSFeatureWriter();
     }
 
     return 0;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runner/JAMSFeatureWriter.cpp	Wed Oct 15 11:33:14 2014 +0100
@@ -0,0 +1,327 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Annotator
+    A utility for batch feature extraction from audio files.
+    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
+    Copyright 2007-2014 QMUL.
+
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "JAMSFeatureWriter.h"
+
+using namespace std;
+using Vamp::Plugin;
+using Vamp::PluginBase;
+
+#include "base/Exceptions.h"
+#include "rdf/PluginRDFIndexer.h"
+
+JAMSFeatureWriter::JAMSFeatureWriter() :
+    FileFeatureWriter(SupportOneFilePerTrackTransform |
+                      SupportOneFilePerTrack |
+                      SupportOneFileTotal |
+		      SupportStdOut,
+                      "json"),
+    m_network(false),
+    m_networkRetrieved(false)
+{
+}
+
+JAMSFeatureWriter::~JAMSFeatureWriter()
+{
+}
+
+string
+JAMSFeatureWriter::getDescription() const
+{
+    return "Write features to JSON files in JAMS (JSON Annotated Music Specification) format.";
+}
+
+JAMSFeatureWriter::ParameterList
+JAMSFeatureWriter::getSupportedParameters() const
+{
+    ParameterList pl = FileFeatureWriter::getSupportedParameters();
+    Parameter p;
+
+    p.name = "network";
+    p.description = "Attempt to retrieve RDF descriptions of plugins from network, if not available locally";
+    p.hasArg = false;
+    pl.push_back(p);
+
+    return pl;
+}
+
+void
+JAMSFeatureWriter::setParameters(map<string, string> &params)
+{
+    FileFeatureWriter::setParameters(params);
+
+    for (map<string, string>::iterator i = params.begin();
+         i != params.end(); ++i) {
+        if (i->first == "network") {
+            m_network = true;
+        }
+    }
+}
+
+void
+JAMSFeatureWriter::setTrackMetadata(QString trackId, TrackMetadata metadata)
+{
+    QString json
+	("\n\"file_metadata\":\n"
+	 "  { \"artist\": \"%1\",\n"
+	 "    \"title\": \"%2\" },\n");
+    m_metadata[trackId] = json.arg(metadata.maker).arg(metadata.title);
+    cerr << "setTrackMetadata: metadata is: " << m_metadata[trackId] << endl;
+}
+
+static double
+realTime2Sec(const Vamp::RealTime &r)
+{
+    return r / Vamp::RealTime(1, 0);
+}
+
+void
+JAMSFeatureWriter::write(QString trackId,
+			 const Transform &transform,
+			 const Plugin::OutputDescriptor& ,
+			 const Plugin::FeatureList& features,
+			 std::string /* summaryType */)
+{
+    QString transformId = transform.getIdentifier();
+
+    QTextStream *sptr = getOutputStream(trackId, transformId);
+    if (!sptr) {
+        throw FailedToOpenOutputStream(trackId, transformId);
+    }
+
+    QTextStream &stream = *sptr;
+
+    TrackTransformPair tt(trackId, transformId);
+    TrackTransformPair targetKey = getFilenameKey(trackId, transformId);
+
+    if (m_startedTargets.find(targetKey) == m_startedTargets.end()) {
+        // Need to write track-level preamble
+        stream << "{" << m_metadata[trackId] << endl;
+        m_startedTargets.insert(targetKey);
+    }
+
+    bool justBegun = false;
+    
+    if (m_data.find(tt) == m_data.end()) {
+
+	identifyTask(transform);
+
+        QString json("\"%1\": [ ");
+        m_data[tt] = json.arg(getTaskKey(m_tasks[transformId]));
+        justBegun = true;
+    }
+
+    QString d = m_data[tt];
+
+    for (int i = 0; i < int(features.size()); ++i) {
+
+        if (i > 0 || !justBegun) {
+            d += ",\n";
+        } else {
+            d += "\n";
+        }
+        
+        d += "    { ";
+	
+        Plugin::Feature f(features[i]);
+
+        switch (m_tasks[transformId]) {
+
+        case ChordTask:
+        case SegmentTask:
+        case NoteTask:
+        case UnknownTask:
+            if (f.hasDuration) {
+                d += QString
+                    ("\"start\": { \"value\": %1 }, "
+                     "\"end\": { \"value\": %2 }")
+                    .arg(realTime2Sec(f.timestamp))
+                    .arg(realTime2Sec
+                         (f.timestamp +
+                          (f.hasDuration ? f.duration : Vamp::RealTime::zeroTime)));
+                break;
+            } else {
+                // don't break; fall through to simpler no-duration case
+            }
+            
+        case BeatTask:
+        case KeyTask:
+        case OnsetTask:
+            d += QString("\"time\": { \"value\": %1 }")
+                .arg(realTime2Sec(f.timestamp));
+            break;
+
+        case MelodyTask:
+        case PitchTask:
+            //!!!
+            break;
+        }
+        
+        if (f.label != "") {
+            d += QString(", \"label\": { \"value\": \"%2\" }")
+                .arg(f.label.c_str());
+        } else if (f.values.size() > 0) {
+            d += QString(", \"label\": { \"value\": \"%2\" }")
+                .arg(f.values[0]);
+        }
+            
+        d += " }";
+    }	
+
+    m_data[tt] = d;
+}
+
+void
+JAMSFeatureWriter::finish()
+{
+    cerr << "Finish called on " << this << endl;
+
+    set<QTextStream *> startedStreams;
+
+    for (DataMap::const_iterator i = m_data.begin();
+         i != m_data.end(); ++i) {
+
+        TrackTransformPair tt = i->first;
+        QString data = i->second;
+
+        QTextStream *sptr = getOutputStream(tt.first, tt.second);
+        if (!sptr) {
+            throw FailedToOpenOutputStream(tt.first, tt.second);
+        }
+
+        if (startedStreams.find(sptr) != startedStreams.end()) {
+            *sptr << "," << endl;
+        }
+        startedStreams.insert(sptr);
+        
+        *sptr << data << "\n  ]";
+    }
+        
+    for (FileStreamMap::const_iterator i = m_streams.begin();
+	 i != m_streams.end(); ++i) {
+	*(i->second) << endl << "}" << endl;
+    }
+
+    m_data.clear();
+    m_startedTargets.clear();
+
+    FileFeatureWriter::finish();
+}
+
+void
+JAMSFeatureWriter::loadRDFDescription(const Transform &transform)
+{
+    QString pluginId = transform.getPluginIdentifier();
+    if (m_rdfDescriptions.find(pluginId) != m_rdfDescriptions.end()) return;
+
+    if (m_network && !m_networkRetrieved) {
+	PluginRDFIndexer::getInstance()->indexConfiguredURLs();
+	m_networkRetrieved = true;
+    }
+
+    m_rdfDescriptions[pluginId] = PluginRDFDescription(pluginId);
+    
+    if (m_rdfDescriptions[pluginId].haveDescription()) {
+	cerr << "NOTE: Have RDF description for plugin ID \""
+	     << pluginId << "\"" << endl;
+    } else {
+	cerr << "NOTE: No RDF description for plugin ID \""
+	     << pluginId << "\"" << endl;
+	if (!m_network) {
+	    cerr << "      Consider using the --json-network option to retrieve plugin descriptions"  << endl;
+	    cerr << "      from the network where possible." << endl;
+	}
+    }
+}
+
+void
+JAMSFeatureWriter::identifyTask(const Transform &transform)
+{
+    QString transformId = transform.getIdentifier();
+    if (m_tasks.find(transformId) != m_tasks.end()) return;
+
+    loadRDFDescription(transform);
+    
+    Task task = UnknownTask;
+
+    QString pluginId = transform.getPluginIdentifier();
+    QString outputId = transform.getOutput();
+
+    const PluginRDFDescription &desc = m_rdfDescriptions[pluginId];
+    
+    if (desc.haveDescription()) {
+
+	PluginRDFDescription::OutputDisposition disp = 
+	    desc.getOutputDisposition(outputId);
+
+	QString af = "http://purl.org/ontology/af/";
+	    
+	if (disp == PluginRDFDescription::OutputSparse) {
+
+	    QString eventUri = desc.getOutputEventTypeURI(outputId);
+
+	    //!!! todo: allow user to prod writer for task type
+
+	    if (eventUri == af + "Note") {
+		task = NoteTask;
+	    } else if (eventUri == af + "Beat") {
+		task = BeatTask;
+	    } else if (eventUri == af + "ChordSegment") {
+		task = ChordTask;
+	    } else if (eventUri == af + "KeyChange") {
+		task = KeyTask;
+	    } else if (eventUri == af + "KeySegment") {
+		task = KeyTask;
+	    } else if (eventUri == af + "Onset") {
+		task = OnsetTask;
+	    } else if (eventUri == af + "NonTonalOnset") {
+		task = OnsetTask;
+	    } else if (eventUri == af + "Segment") {
+		task = SegmentTask;
+	    } else if (eventUri == af + "SpeechSegment") {
+		task = SegmentTask;
+	    } else if (eventUri == af + "StructuralSegment") {
+		task = SegmentTask;
+	    } else {
+		cerr << "WARNING: Unsupported event type URI <" 
+		     << eventUri << ">, proceeding with UnknownTask type"
+		     << endl;
+	    }
+
+	} else {
+
+	    cerr << "WARNING: Cannot currently write dense or track-level outputs to JSON format (only sparse ones). Will proceed using UnknownTask type, but this probably isn't going to work" << endl;
+	}
+    }	    
+
+    m_tasks[transformId] = task;
+}
+
+QString
+JAMSFeatureWriter::getTaskKey(Task task) 
+{
+    switch (task) {
+    case UnknownTask: return "unknown";
+    case BeatTask: return "beat";
+    case OnsetTask: return "onset";
+    case ChordTask: return "chord";
+    case SegmentTask: return "segment";
+    case KeyTask: return "key";
+    case NoteTask: return "note";
+    case MelodyTask: return "melody";
+    case PitchTask: return "pitch";
+    }
+    return "unknown";
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runner/JAMSFeatureWriter.h	Wed Oct 15 11:33:14 2014 +0100
@@ -0,0 +1,87 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Annotator
+    A utility for batch feature extraction from audio files.
+
+    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
+    Copyright 2007-2014 QMUL.
+
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _JAMS_FEATURE_WRITER_H_
+#define _JAMS_FEATURE_WRITER_H_
+
+#include "transform/FileFeatureWriter.h"
+
+#include "rdf/PluginRDFDescription.h"
+
+class JAMSFileWriter;
+
+class JAMSFeatureWriter : public FileFeatureWriter
+{
+public:
+    JAMSFeatureWriter();
+    virtual ~JAMSFeatureWriter();
+
+    string getDescription() const;
+
+    virtual ParameterList getSupportedParameters() const;
+    virtual void setParameters(map<string, string> &params);
+
+    virtual void setTrackMetadata(QString trackid, TrackMetadata metadata);
+
+    virtual void write(QString trackid,
+                       const Transform &transform,
+                       const Vamp::Plugin::OutputDescriptor &output,
+                       const Vamp::Plugin::FeatureList &features,
+                       std::string summaryType = "");
+
+    virtual void finish();
+
+    virtual QString getWriterTag() const { return "json"; }
+
+private:
+    enum Task {
+	UnknownTask,
+	BeatTask,
+	OnsetTask,
+	ChordTask,
+	SegmentTask,
+	KeyTask,
+	NoteTask,
+	MelodyTask,
+	PitchTask,
+    };
+
+    typedef map<QString, PluginRDFDescription> RDFDescriptionMap; // by plugin id
+    RDFDescriptionMap m_rdfDescriptions;
+
+    typedef map<QString, QString> TrackMetadataMap; // track id -> json object
+    TrackMetadataMap m_metadata;
+
+    typedef map<TrackTransformPair, QString> DataMap;
+    DataMap m_data;
+
+    typedef map<QString, Task> TaskMap; // by transform id
+    TaskMap m_tasks;
+
+    typedef set<TrackTransformPair> StartedSet;
+    StartedSet m_startedTargets;
+
+    void loadRDFDescription(const Transform &);
+    void identifyTask(const Transform &);
+
+    QString getTaskKey(Task);
+
+    bool m_network;
+    bool m_networkRetrieved;
+};
+
+#endif
+
--- a/runner/MIDIFeatureWriter.cpp	Wed Oct 15 11:23:05 2014 +0100
+++ b/runner/MIDIFeatureWriter.cpp	Wed Oct 15 11:33:14 2014 +0100
@@ -165,5 +165,9 @@
             }
 	}
     }
+
+    m_notes.clear();
+
+    FileFeatureWriter::finish();
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-json-destinations/test-json-destinations.sh	Wed Oct 15 11:33:14 2014 +0100
@@ -0,0 +1,181 @@
+#!/bin/bash
+
+. ../include.sh
+
+infile1=$audiopath/3clicks8.wav
+infile2=$audiopath/6clicks8.wav
+
+infile1dot=$audiopath/3.clicks.8.wav
+
+outfile1=3clicks8.json
+outfile2=6clicks8.json
+
+outfile3=3clicks8_vamp_vamp-example-plugins_percussiononsets_onsets.json
+outfile4=3clicks8_vamp_vamp-example-plugins_percussiononsets_detectionfunction.json
+outfile5=6clicks8_vamp_vamp-example-plugins_percussiononsets_onsets.json
+outfile6=6clicks8_vamp_vamp-example-plugins_percussiononsets_detectionfunction.json
+
+outfile1dot=3.clicks.8.json
+
+tmpjson=$mypath/tmp_1_$$.json
+
+trap "rm -f $tmpjson $outfile1 $outfile2 $outfile3 $outfile4 $outfile5 $outfile6 $infile1dot $outfile1dot $audiopath/$outfile1 $audiopath/$outfile2 $audiopath/$outfile3 $audiopath/$outfile4 $audiopath/$outfile5 $audiopath/$outfile6 $audiopath/$outfile1dot" 0
+
+transformdir=$mypath/transforms
+
+check_json() {
+    test -f $1 || \
+	fail "Fails to write output to expected location $1 for $2"
+    cat $1 | json_verify || \
+	fail "Writes invalid JSON to location $1 for $2"
+    rm -f $1
+}    
+
+
+ctx="onsets transform, one audio file, default JSON writer destination"
+
+rm -f $audiopath/$outfile1
+
+$r -t $transformdir/onsets.n3 -w json $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1 "$ctx"
+
+
+ctx="onsets transform, one audio file with dots in filename, default JSON writer destination"
+
+rm -f $audiopath/$outfile1
+
+cp $infile1 $infile1dot
+
+$r -t $transformdir/onsets.n3 -w json $infile1dot 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1dot "$ctx"
+
+rm -f $infile1dot $audiopath/$outfile1dot
+
+
+ctx="onsets and df transforms, one audio file, default JSON writer destination"
+
+rm -f $audiopath/$outfile1
+
+$r -t $transformdir/onsets.n3 -t $transformdir/detectionfunction.n3 -w json $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1 "$ctx"
+
+
+ctx="onsets transform, two audio files, default JSON writer destination"
+
+rm -f $audiopath/$outfile1
+rm -f $audiopath/$outfile2
+
+$r -t $transformdir/onsets.n3 -w json $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1 "$ctx"
+check_json $audiopath/$outfile2 "$ctx"
+
+
+ctx="onsets transform, two audio files, one-file JSON writer"
+
+$r -t $transformdir/onsets.n3 -w json --json-one-file $tmpjson $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $tmpjson "$ctx"
+
+
+ctx="onsets transform, two audio files, stdout JSON writer"
+
+$r -t $transformdir/onsets.n3 -w json --json-stdout $infile1 $infile2 2>/dev/null >$tmpjson || \
+    fail "Fails to run with $ctx"
+
+check_json $tmpjson "$ctx"
+
+
+ctx="onsets transform, one audio file, many-files JSON writer"
+
+rm -f $audiopath/$outfile3
+
+$r -t $transformdir/onsets.n3 -w json --json-many-files $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile3 "$ctx"
+
+
+ctx="onsets transform, two audio files, many-files JSON writer"
+
+rm -f $audiopath/$outfile3
+rm -f $audiopath/$outfile5
+
+$r -t $transformdir/onsets.n3 -w json --json-many-files $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile3 "$ctx"
+check_json $audiopath/$outfile5 "$ctx"
+
+
+ctx="onsets and df transforms, two audio files, many-files JSON writer"
+
+rm -f $audiopath/$outfile3
+rm -f $audiopath/$outfile4
+rm -f $audiopath/$outfile5
+rm -f $audiopath/$outfile6
+
+$r -t $transformdir/onsets.n3 -t $transformdir/detectionfunction.n3 -w json --json-many-files $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile3 "$ctx"
+check_json $audiopath/$outfile4 "$ctx"
+check_json $audiopath/$outfile5 "$ctx"
+check_json $audiopath/$outfile6 "$ctx"
+
+
+ctx="output base directory"
+
+rm -f ./$outfile1
+
+$r -t $transformdir/onsets.n3 -t $transformdir/detectionfunction.n3 -w json --json-basedir . $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json ./$outfile1 "$ctx"
+
+
+ctx="output base directory and many-files"
+
+rm -f ./$outfile3
+rm -f ./$outfile5
+
+$r -t $transformdir/onsets.n3 -w json --json-basedir . --json-many-files $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json ./$outfile3 "$ctx"
+check_json ./$outfile5 "$ctx"
+
+
+ctx="nonexistent output base directory"
+
+$r -t $transformdir/onsets.n3 -w json --json-basedir ./DOES_NOT_EXIST $infile1 2>/dev/null && \
+    fail "Fails with $ctx by completing successfully (should refuse and bail out)"
+
+
+ctx="existing output file and no --json-force"
+
+touch $audiopath/$outfile1
+
+$r -t $transformdir/onsets.n3 -w json $infile1 2>/dev/null && \
+    fail "Fails by completing successfully when output file already exists (should refuse and bail out)"
+
+
+ctx="existing output file and --json-force"
+
+touch $audiopath/$outfile1
+
+$r -t $transformdir/onsets.n3 -w json --json-force $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1 "$ctx"
+
+
+exit 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-json-destinations/transforms/detectionfunction.n3	Wed Oct 15 11:33:14 2014 +0100
@@ -0,0 +1,11 @@
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
+@prefix vamp: <http://purl.org/ontology/vamp/>.
+@prefix examples: <http://vamp-plugins.org/rdf/plugins/vamp-example-plugins#>.
+@prefix : <#>.
+
+:transform0 a vamp:Transform;
+	vamp:plugin examples:percussiononsets ;
+	vamp:output examples:percussiononsets_output_detectionfunction .
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-json-destinations/transforms/onsets.n3	Wed Oct 15 11:33:14 2014 +0100
@@ -0,0 +1,10 @@
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
+@prefix vamp: <http://purl.org/ontology/vamp/>.
+@prefix examples: <http://vamp-plugins.org/rdf/plugins/vamp-example-plugins#>.
+@prefix : <#>.
+
+:transform0 a vamp:Transform;
+	vamp:plugin examples:percussiononsets.
+
+
+
--- a/tests/test-supportprogs/test-supportprogs.sh	Wed Oct 15 11:23:05 2014 +0100
+++ b/tests/test-supportprogs/test-supportprogs.sh	Wed Oct 15 11:33:14 2014 +0100
@@ -11,5 +11,8 @@
 rapper --version >/dev/null || \
     fail "Can't find required rapper program"
 
+echo '{}' | json_verify >/dev/null || \
+    fail "Can't find required json_verify program, or it doesn't seem to work"
+
 exit 0
 
--- a/tests/test.sh	Wed Oct 15 11:23:05 2014 +0100
+++ b/tests/test.sh	Wed Oct 15 11:33:14 2014 +0100
@@ -16,6 +16,7 @@
     lab-writer \
     lab-destinations \
     midi-destinations \
+    json-destinations \
     summaries \
     multiple-audio \
     ; do