Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

A Sound File Editor for Netbeans


March, 2005: A Sound File Editor for Netbeans

A full-featured Java IDE built on top of the Netbeans Platform

Rich is a software engineer at Nuance Communications, and a member of the Netbeans Governance Board. He can be reached at [email protected].


I am on the development team for V-Builder, an IDE for building VoiceXML-based speech applications. Among other features, we had to provide V-Builder with the ability to edit several file types you don't normally associate with IDEs—call flows, VoiceXML files, linguistic grammars, recorded prompts, and the like. To implement these features, we turned to Netbeans, an open-source framework for building Java client applications (http:// www.netbeans.org/), mainly because much of the necessary functionality for IDEs is already implemented in Netbeans. It has a windowing system, JavaHelp integration, source control, syntax coloring and completion, generic XML editing, and lots of other goodies. Consequently, we decided to implement our application as a set of Netbeans plug-in modules.

For instance, one of our modules is a prompt editor. In the context of a speech application, a prompt is a file containing information about questions the application may ask users. The information includes:

  • A transcript.
  • Instructions for the voice talent recording the prompt (optional).
  • The recorded .wav file (optional).

The first two items are stored in a Java properties file with the extension "prompt." An important design consideration is that, because applications are designed before the voice talent records the prompts, it is possible to have the prompt file without the .wav file. So, our Netbeans module must be able to recognize prompt files by themselves, and associate them with sibling .wav files in a seamless fashion.

The Anatomy of a Module

A Netbeans module is a jar file with an enhanced manifest. To turn any existing jar file into a Netbeans module, you add two lines to the jar's manifest:

OpenIDE-Module: org.netbeans.modules .mymodule/1
OpenIDE-Module-Specification-Version: 1.0

This is sufficient for Netbeans to install, recognize the code in that jar file, and assign a class loader to create instances of the classes defined there. Of course, I needed to do more than this to get my code to integrate into the Netbeans UI. The prompt editor's manifest (Listing One) declares a few more fields that affect the runtime behavior of the module. All Netbeans-specific entries in the manifest begin with "OpenIDE-Module."

The OpenIDE-Module-IDE-Dependencies field indicates that the module requires Version 4.41 of the core framework (which corresponds to Netbeans 4.0). The OpenIDE-Module-Module-Dependencies field declares dependencies on classes from other modules. For example, I use the org.openide.io module to print information to the output window. The OpenIDE-Module-Localizing-Bundle field points to a properties file containing manifest entries that may be translated into different languages, such as the title, description, and category of this module (Listing Two). The Class-Path entry adds jar files to the module's class loader. In this case, the only declared jar file contains a JavaHelp help set, which can be loaded from menu items or Netbeans' context-sensitive help system.

The OpenIDE-Module-Layer field indicates the location of the layer file, an XML document that declares how the UI components integrate with the rest of the framework. Netbeans treats its UI like a filesystem. For example, the Menus folder contains a folder for each top-level menu. These can, in turn, contain folders (submenus) or files (menu items). The prompt editor's layer file (Listing Three) is an XML representation of part of this filesystem. At startup, Netbeans takes all the modules' layers and merges them together to create the complete picture.

The final two lines of my manifest declare a DataLoader class, which describes a file type. Netbeans maintains a loader pool, which scans files in a given directory, grouping the files into logical chunks, or just determining what type of data each represents. Most DataLoaders extend UniFileLoader, and recognize individual files based on their extensions or MIME types. The PromptLoader (Listing Four) is a MultiFileLoader, because I want a .wav file and prompt file to appear as a single item in the file explorer. The findPrimaryFile() method knows how to pair the files together. The createMultiObject() method knows how to create a DataObject from a prompt file.

A DataObject represents a particular instance of a file or group of files. A PromptDataObject (Listing Five, available electronically; see "Resource Center," page 5) represents a single prompt file and its associated .wav file. The DataObject is responsible for encapsulating the relevant data, as well as creating node representations of the DataObject, tracking whether the data has been modified since the last save, and maintaining a set of cookies.

Cookies are capabilities (open, save, edit, print, and so on) that are different from actions. A cookie represents the capability to perform an operation. The action represents the UI component for performing the operation. For example, a PromptNode always has a SaveAction associated with it. The SaveCookie, however, is added or removed from the PromptDataObject when it is modified or saved. This is because users should not be able to save files that have not been modified. The action remains, even when the cookie is gone. The result is that the action is disabled (grayed-out).

Prompts are edited using a PromptEditor (Listing Six, available electronically), which is an instance of TopComponent. A TopComponent is a JComponent that is managed by the Netbeans window manager. Most top components in Netbeans actually subclass CloneableTopComponent, which adds the ability to create more than one view of the same DataObject.

The PromptEditor has a single member variable—an EditorPanel. This is the actual implementation of the editor, which could be referenced from a JFrame in a standalone Swing application, or (in my case) from the PromptEditor. The important things to override in a TopComponent are the open/close behavior and serialization routines. The open() hook in PromptEditor simply defers to the open behavior in EditorPanel, which renders the audio waveform, transcript, and instructions. The canClose() hook is the usual place to check if the underlying data was modified, and save (or ask to save) as necessary.

Serialization is controlled with three hooks. The readExternal() and writeExternal() hooks are not specific to Netbeans, and should be familiar to anyone writing Java GUI applications. The third, getPersistenceType(), controls whether Netbeans should bother to serialize a TopComponent on shutdown. There are three possible values: PERSISTENCE_NEVER indicates that this component should not be restored upon restarting Netbeans; PERSISTENCE_ONLY_OPENED indicates that the component should only be restored if it was visible when Netbeans was shut down; PERSISTENCE_ALWAYS indicates that the component should always remember where the window was docked, even if it was closed at the time Netbeans exited.

Testing the Prompt Editor

By creating a jar file from this source code, layer file, and manifest, you have a complete, integrated Netbeans module. The quickest way to test this module is from within Netbeans. The Tools|Options menu item displays a tree view of configuration options. In this tree, under IDE Configuration|System, there is a node called "Modules." Right-click this node and select Add|Module, then select the jar file in the resulting file dialog.

Now you can select File|New File from the menu and see Prompt as one of the templates (Figure 1). Create a prompt this way, and you should be able to edit it by double-clicking it (Figure 2). Also note the help set referred to in the layer file (Figure 3).

To package the prompt editor for distribution, the usual method is to create a Netbeans module package. This is essentially a signed jar with the extension "nbm," which contains the module jar file and any associated resources to be distributed along with the code. The prompt editor includes the JavaHelp help set this way. Netbeans provides an Ant task called "<makenbm>" that packages everything for you.

Conclusion

The prompt module exercises just a few of the many integration points modules can make with the Netbeans framework. Building client applications on top of Netbeans lets you concentrate on writing the functionality necessary to address your core competencies, rather than reinventing the GUI application wheel.

V-Builder is a good example of leveraging the entire Netbeans IDE to create an IDE for building something besides Java applications. However, the Netbeans Platform is also an excellent framework for building applications that are not IDEs.

DDJ



Listing One

Manifest-Version: 1.0
OpenIDE-Module: com.nuance.tools.prompt/1

OpenIDE-Module-Specification-Version: 1.0
OpenIDE-Module-IDE-Dependencies: IDE/1 > 4.41
OpenIDE-Module-Module-Dependencies: org.openide.io
OpenIDE-Module-Localizing-Bundle: 
                     com/nuance/tools/prompt/resources/Bundle.properties
OpenIDE-Module-Layer: com/nuance/tools/prompt/resources/mf-layer.xml
Class-Path: docs/com-nuance-tools-prompt-edit.jar

Name: com/nuance/tools/prompt/PromptLoader.class
OpenIDE-Module-Class: Loader
Back to article


Listing Two
# Bundle.properties
# moved from the module manifest, so they can be localized

OpenIDE-Module-Name=Prompt Editor
OpenIDE-Module-Short-Description=Prompt Editor
OpenIDE-Module-Long-Description=
             Use this module to record, play, crop and normalize audio files.
OpenIDE-Module-Implementation-Title=V-Builder
OpenIDE-Module-Implementation-Vendor=Nuance
OpenIDE-Module-Display-Category=V-Builder
Back to article


Listing Three
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC '-//NetBeans//DTD Filesystem 1.0//EN' 
                            'http://www.netbeans.org/dtds/filesystem-1_0.dtd'>
<filesystem>
  <!-- The "File...New File" templates -->
  <folder name="Templates">
    <folder name="V-Builder">
      <!-- empty.prompt is a file in the same directory as this layer file -->
      <file name="untitled.prompt" url="empty.prompt">
        <attr name="template" 
              boolvalue="true"/>
        <attr name="SystemFileSystem.localizingBundle" 
              stringvalue="com.nuance.tools.prompt.Bundle" />
        <attr name="SystemFileSystem.icon" urlvalue=
             "nbresloc:/com/nuance/tools/prompt/resources/wavIconSmall.gif"/> 
        <attr name="templateWizardURL" urlvalue=
             "nbresloc:/com/nuance/tools/prompt/resources/templatesWav.html"/> 
      </file>
    </folder>
  </folder>
  <!-- register the JavaHelp help set -->
  <folder name="Services">
    <folder name="JavaHelp">
        <file name="com-nuance-tools-prompt-edit-helpset.xml">
        <![CDATA[<?xml version="1.0"?>
            <!DOCTYPE helpsetref PUBLIC
            "-//NetBeans//DTD JavaHelp Help Set Reference 1.0//EN"
            "http://www.netbeans.org/dtds/helpsetref-1_0.dtd">
            <helpsetref url="nbdocs:/com-nuance-tools-prompt-edit/
                                          com-nuance-tools-prompt-edit.hs"/>
        ]]>
        </file>
    </folder>
  </folder>
  <!-- make a menu item for the help set -->
  <folder name="Menu">
    <folder name="Help">
      <!-- Put "V-Builder" sub-menu before the "Help Contents" sub-menu -->
      <attr name="V-Builder/HelpShortcuts" boolvalue="true"/> 
      <folder name="V-Builder">
        <attr name="SystemFileSystem.icon" urlvalue=
              "nbresloc:/org/netbeans/modules/javahelp/resources/help.gif"/> 
        <file name="com-nuance-tools-prompt-edit-help-menu.xml">
          <![CDATA[<?xml version="1.0"?>
          <!DOCTYPE helpctx PUBLIC "-//NetBeans//DTD Help Context 1.0//EN"
          "http://www.netbeans.org/dtds/helpcontext-1_0.dtd">
          <helpctx id="com.nuance.tools.prompt.edit" showmaster="false"/>
          ]]>
          <attr name="SystemFileSystem.localizingBundle" 
                stringvalue="com.nuance.tools.prompt.resources.Bundle"/>
          <attr name="SystemFileSystem.icon" urlvalue=
               "nbresloc:/com/nuance/tools/prompt/resources/wavIconSmall.gif"/>
        </file>
      </folder>
    </folder>
  </folder>
</filesystem>
Back to article


Listing Four
package com.nuance.tools.prompt;

import java.io.IOException;

import org.openide.actions.*;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObjectExistsException;
import org.openide.loaders.ExtensionList;
import org.openide.loaders.FileEntry;
import org.openide.loaders.MultiDataObject;
import org.openide.loaders.MultiFileLoader;
import org.openide.util.NbBundle;
import org.openide.util.actions.SystemAction;

/**  Recognizes .prompt and .wav files as a single DataObject.
 * .prompt files are the primary file objects. @author Rich Unger
 */
public class PromptLoader extends MultiFileLoader {
    public static final String PROP_EXTENSIONS = "extensions"; // NOI18N
    public static final String WAV_EXTENSION = "wav";
    public static final String INFO_FILE_EXTENSION = "prompt";
    private static final long serialVersionUID = -4579746482156153693L;
    public PromptLoader() {
        super("com.nuance.tools.prompt.PromptDataObject");
    }
    protected String defaultDisplayName () {
        return NbBundle.getMessage(PromptLoader.class, "LBL_loaderName");
    }
    protected SystemAction[] defaultActions () {
        return new SystemAction[] {
            SystemAction.get (OpenAction.class),
            SystemAction.get (FileSystemAction.class), null,
            SystemAction.get (CutAction.class),
            SystemAction.get (CopyAction.class),
            SystemAction.get (PasteAction.class), null,
            SystemAction.get (DeleteAction.class),
            SystemAction.get (RenameAction.class), null,
            SystemAction.get (PropertiesAction.class),
        };
    }
    protected MultiDataObject createMultiObject (FileObject primaryFile)
    throws DataObjectExistsException, IOException {
        return new PromptDataObject(primaryFile, this);
    }
    /** For a given file find the primary file. @param fo the file to find 
     * the primary file for @return the primary file for this file or null 
     * if this file is not recognized by this loader.
     */
    protected FileObject findPrimaryFile(FileObject fo) {
        // never recognize folders.
        if (fo.isFolder()) return null;
        String ext = fo.getExt();
        if (ext.equalsIgnoreCase(WAV_EXTENSION)) {
            FileObject info = FileUtil.findBrother(fo, INFO_FILE_EXTENSION);
            if(info != null) {
                return info;
            } 
            else {
                try {
                    info = fo.getParent().createData(
                            fo.getName(), INFO_FILE_EXTENSION);
                    return info;
                } catch(IOException ioe) {
                    // could not create .prompt file, 
                    // so cannot recognize .wav file
                    return null;
                }
            }
        }
        if (getExtensions().isRegistered(fo)) {
            return fo;
        }
        return null;
    }
    /** Create the primary file entry. Primary files are the property files 
     * (which contain the prompt's * description and recording instructions).
     * @param primaryFile primary file recognized by this loader
     * @return primary entry for that file
     */
    protected MultiDataObject.Entry createPrimaryEntry(
            MultiDataObject obj, FileObject primaryFile) {
        return new FileEntry(obj, primaryFile);
    }
    /** Create a secondary file entry.
     * Secondary files are wav files, which should also be retained (so, not a
     * FileEntry.Numb object)
     * @param secondaryFile secondary file to create entry for
     * @return the entry
     */
    protected MultiDataObject.Entry createSecondaryEntry(
            MultiDataObject obj, FileObject secondaryFile) {
        return new FileEntry(obj, secondaryFile);
    }
    /** @return The list of extensions this loader recognizes. */
    public ExtensionList getExtensions() {
        ExtensionList extensions =(ExtensionList)getProperty(PROP_EXTENSIONS);
        if (extensions == null) {
            extensions = new ExtensionList();
            extensions.addExtension(INFO_FILE_EXTENSION);
            extensions.addExtension(WAV_EXTENSION);
            putProperty(PROP_EXTENSIONS, extensions, false);
        }
        return extensions;
    }
    /** Sets the extension list for this data loader.
     * @param ext new list of extensions.
     */
    public void setExtensions(ExtensionList ext) {
        putProperty(PROP_EXTENSIONS, ext, true);
    }
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.