Programmatically editing the metadata of an audio file


Today's Little Program edits the metadata of an audio file, ostensibly to correct a spelling error, but really just to show how it's done.

Today's smart pointer class library is... (rolls dice)... CComPtr!

We open with two helper functions which encapsulate the patterns

  • Get property from property store
    1. Call IProperty­Store::Get­Value
    2. Convert PROPVARIANT into desired final type
    3. Destroy the PROPVARIANT
  • Set property in property store
    1. Create a PROPVARIANT
    2. Call IProperty­Store::Set­Value
    3. Destroy the PROPVARIANT
#define STRICT
#include <windows.h>
#include <stdio.h>
#include <shlobj.h>
#include <propkey.h>
#include <propvarutil.h>
#include <atlbase.h>
#include <atlalloc.h>

template<typename TLambda>
HRESULT GetPropertyAsLambda(IPropertyStore *pps, REFPROPERTYKEY key,
                             TLambda lambda)
{
  PROPVARIANT pvar;
  HRESULT hr = pps->GetValue(key, &pvar);
  if (SUCCEEDED(hr)) {
    hr = lambda(pvar);
    PropVariantClear(&pvar);
  }
  return hr;
}

template<typename TLambda>
HRESULT SetPropertyAsLambda(IPropertyStore *pps, REFPROPERTYKEY key,
                            TLambda lambda)
{
  PROPVARIANT pvar;
  HRESULT hr = lambda(&pvar);
  if (SUCCEEDED(hr)) {
    hr = pps->SetValue(key, pvar);
    PropVariantClear(&pvar);
  }
  return hr;
}

Both functions use a lambda to do the type-specific work.

Here are some functions that will use the helpers:

HRESULT GetPropertyAsString(
    IPropertyStore *pps, REFPROPERTYKEY key, PWSTR *ppszValue)
{
  *ppszValue = nullptr;
  return GetPropertyAsLambda(pps, key, [=](REFPROPVARIANT pvar) {
    return PropVariantToStringAlloc(pvar, ppszValue);
  });
}

HRESULT SetPropertyAsString(
    IPropertyStore *pps, REFPROPERTYKEY key, PCWSTR pszValue)
{
  return SetPropertyAsLambda(pps, key, [=](PROPVARIANT *ppvar) {
    return InitPropVariantFromString(pszValue, ppvar);
  });
}

HRESULT GetPropertyAsStringVector(
    IPropertyStore *pps, REFPROPERTYKEY key,
    PWSTR **pprgsz, ULONG *pcElem)
{
  *pprgsz = nullptr;
  *pcElem = 0;
  return GetPropertyAsLambda(pps, key, [=](REFPROPVARIANT pvar) {
    return PropVariantToStringVectorAlloc(pvar, pprgsz, pcElem);
  });
}

HRESULT SetPropertyAsStringVector(
    IPropertyStore *pps, REFPROPERTYKEY key,
    PCWSTR *prgsz, ULONG cElems)
{
  return SetPropertyAsLambda(pps, key, [=](PROPVARIANT *ppvar) {
    return InitPropVariantFromStringVector(prgsz, cElems, ppvar);
  });
}

The Prop­Variant­To­String­Vector­Alloc function returns an array of pointers to memory allocated via Co­Task­Mem­Alloc, and the array itself was also allocated by the same function. Here's a helper function to free the memory and the array:

template<typename T>
void CoTaskMemFreeArray(T **prgElem, ULONG cElem)
{
    for (ULONG i = 0; i < cElem; i++) {
        CoTaskMemFree(prgElem[i]);
    }
    CoTaskMemFree(prgElem);
}

Okay, we're ready to write our main program. Remember, Little Programs do little to no error checking. In a real program, you would check that your function calls succeeded.

int __cdecl wmain(int argc, wchar_t **argv)
{
  CCoInitialize init;
  CComPtr<IPropertyStore> spps;
  SHGetPropertyStoreFromParsingName(argv[1], nullptr,
    GPS_READWRITE, IID_PPV_ARGS(&spps));

  // Get the existing composers
  PWSTR *rgpszComposers;
  ULONG cComposers;
  GetPropertyAsStringVector(spps, PKEY_Music_Composer,
    &rgpszComposers, &cComposers);

  // Look for "Dvorak, Antonin" and add diacritics
  for (ULONG ulPos = 0; ulPos < cComposers; ulPos++) {
    if (wcscmp(rgpszComposers[ulPos], L"Dvorak, Antonin") == 0) {
      // Swap in the new name
      PWSTR pszOld = rgpszComposers[ulPos];
      rgpszComposers[ulPos] = L"Dvo\x0159\x00E1k, Anton\x00EDn";
      // Write out the new list of composers
      SetPropertyAsStringVector(spps, PKEY_Music_Composer, (PCWSTR *)rgpszComposers, cComposers);
      // Swap it back so we can free it
      rgpszComposers[ulPos] = pszOld;
      // Add a little graffiti just because
      SetPropertyAsString(spps, PKEY_Comment, L"Kilroy was here");
      spps->Commit();
      break;
    }
  }
  CoTaskMemFreeArray(rgpszComposers, cComposers);
  
  return 0;
}

Okay, what just happened here?

First, we took the file whose name was passed on the command line (fully-qualified path, please) and obtained its property store.

Next, we queried the property store for the System.­Music.­Composer property. This property is typed as a multiple-valued string, so we read and write the value in the form of a string vector. You could also read and write it as a single string: The Prop­Variant­To­String­Alloc function represents string arrays by joining the strings together, separating them with "; " (semicolon and space). However, we access it as an array because that makes it easier to insert and remove individual entries.

Once we get the list of composers, we look for one that says "Dvorak, Antonin". If we find it, then we change that entry to "Dvořák, Antonín" and write out the new vector.

And then just to show that I know how to write out a string property too, I'll put some graffiti in the Comment field.

Commit the changes and break the loop now that we found what we're looking for. (This assumes that the song was not a collaboration between Antonín Dvořák and himself!)

So there you have it, a little program that modifies metadata. Obviously, this program is not particularly useful by itself, but it illustrates what you need to do to do something more useful in general.

Comments (12)
  1. alegr1 says:

    You can use the wonderful new feature of Visual C, and save your source file as Unicode, so you don't have to enter non-ASCII characters by their hex codes.

  2. Mark says:

    alegr1: and then it stops displaying properly on some terminals, doesn't round trip through some command-line programs and mail clients, and makes it awkward to reason about the memory contents. Better to pull the strings out into a separate file if you want to edit them that way.

  3. JDP says:

    is that a for-if loop I spy, Raymond? :o)

    I know, little program, example purposes only — but once you explained that as an anti-pattern, it was like a switch being flipped in my head, and I wind up doing my best to make any for-ifs that I find more sensible.

  4. me says:

    hopefully that doesnt have the nasty side effect with txxx tags that has been in there since 2009…

  5. The for-if antipattern looks like this:

    for (i = some range of values) {

    ____ if (i == some specific value) { do something with i }

    }

    This can be replaced with the more performant

    do something with (the specific value)

    That is not the case in this example. What we have here is:

    for (i = some range of values) {

    ____ if (array[i] == some specific value) { do something with i }

    }

    There is no way to replace this with a single statement. If we were guaranteed that the "composers" array were sorted we could do a binary search instead of a linear search, but no such guarantee exists.

  6. Jodi says:

    I think he meant not seeing the general usefulness of it due to unsupported formats that are commonly used.

    Then again he might have confused it with the metadata stored in alternative streams or with metadata locked in to individual user accounts. Dealing with COM, metadata can mean a lot of things. Like sending your boss that photoshopped picture of him that you made. Lovely bugs and office installed. I've run windows 7 for years I never knew it supported flac. I just tested it. I'm surprised. Must be something I installed, surely. It do still however not support mkv or ogg. As for me I did go nutsy, windows media player, ugh, unusable and slow! After that episode I installed an open source player.

  7. Peter M says:

    // Swap in the new name

    PWSTR pszOld = rgpszComposers[ulPos];

    rgpszComposers[ulPos] = L"Dvox0159x00E1k, Antonx00EDn";

    // Swap it back so we can free it

    rgpszComposers[ulPos] = pszOld;

    Swapping pointers to allocated memory make me nervous.

    Swapping a pointer with static string when talking to com makes me worried

    rgpszComposers[ulPos] = L"Dvox0159x00E1k, Antonx00EDn";

    "but it illustrates what you need to do to do something more useful in general."

    No, I'm not seeing it. An example would have been nice.

    I only see problems: Since when can windows read flac and mkv metadata? Can it even read ogg?

    Windows version of "metadata", *cough* time wasting lock-in *cough* is not compatible when you move the files to another computer eg Linux or even sometimes another windows machine.

    [This was a quick and dirty app, so it cuts a lot of corners, such as editing the array in place. I'm not sure what you mean by "I'm not seeing it." I showed how you can read and modify the Composers property of an audio file. What's not to see? (Windows stores it in the ID3 field of the mp3 file. Isn't that widely recognized? And if you want to write a flac or mkv property provider, go nutsy.) -Raymond]
  8. @Jodi:

    The formats are third party, so Windows doesn't support them out of the box.

    There is, however, nothing stopping third party developers from providing the same level of integration to these formats that the likes of MP3 and WMA have.

    For Direct Show, LAV Filters provides splitters and filters for a lot of out of box formats like flac/mkv/ogg and more. This doesn't add general support for the tags that these formats use though. As Raymond said, someone needs to write a property provider for these formats, and that is just something that people haven't done.

  9. parkrrrr says:

    How appropriate that the Classical Music Library Free Download of the Week for this week (or week-like period) is Dvořák's Symphony No. 9.

  10. > "but it illustrates what you need to do to do something more useful in general."

    > No, I'm not seeing it. An example would have been nice.

    As an example of a practical use of this technique, here's a tool I use to groom the audio metadata in my collection (mostly to set the album title on content which spans multiple CDs.)

    blogs.msdn.com/…/shellproperty-exe-set-read-string-properties-on-a-file-from-the-command-line.aspx

  11. NitpickersParadise says:

    ITT: People complaining that relatively obscure* formats are not supported out of the box by Windows. Oh the humanity! How will I ever properly tag my APE files now?

    *In sheer numbers MP3 will be here today, here tomorrow, here forever. The wise-man however always archives with FLAC then transcodes to the flavor of the week, but how many people are that forward thinking?

    The solution, as usual is to look the the wide world of third party utilities that are what makes the Windows Ecosystem the Goliath it has been for years in the desktop world. Personally I enjoy Mp3Tag.de which supports *some* of these obscure formats.

  12. Bulletmagnet says:

    Did you actually roll dice, or did you use http://xkcd.com/221/ ?

Comments are closed.

Skip to main content