Building a Cross-Platform Music Tag Editor with Flutter

Read, edit, and organize audio metadata across Android, iOS, Windows, macOS, Linux, and Web.

Have you ever downloaded a song and found something like this?

Title:        unknown
Artist:       Unknown Artist
Album:        -
Genre:        -
Track:        0
Year:         0
Album Art:    missing

Or perhaps you've got a perfectly organized music library except for 2,000 songs with inconsistent artist names, missing album artwork, and track numbers that look like they were entered by five different people.

At some point, you need a music tag editor.

In this tutorial, we're going to build one using Flutter and hAudiotagger.

By the end, we'll have a small application capable of:

  • Selecting an audio file
  • Reading its metadata
  • Displaying title, artist, album, genre, year, and track information
  • Displaying embedded album artwork
  • Editing metadata
  • Saving the changes back to the file
  • Working across Flutter platforms
  • Separating the UI from the metadata engine

The interesting part is that the UI is Flutter, while the metadata engine is powered by Rust.

Let's build it. 🎵


Try the Live Demo

Want to see the result before building it yourself?

Try the hAudiotagger Web demo:
https://haudiotagger.hirdaya-shrestha.com.np/

The live demo runs directly in your browser and lets you experiment with the music metadata editor without installing anything.

Open the hAudiotagger Live Demo

Note: Because this is a Web application, audio files are selected through your browser rather than accessed directly from your filesystem.


What we're building

Our finished application will look conceptually like this:

┌───────────────────────────────────────────────────┐
│                 Music Tag Editor                  │
├───────────────────────────────────────────────────┤
│                                                   │
│     ┌──────────────┐                              │
│     │              │     Song Title               │
│     │  Album Art   │     Artist Name              │
│     │              │     Album Name               │
│     │              │     Genre                    │
│     └──────────────┘     Year                     │
│                                                   │
│     ─────────────────────────────────────────     │
│                                                   │
│     Title    [ Never Gonna Give You Up        ]   │
│     Artist   [ Rick Astley                    ]   │
│     Album    [ Whenever You Need Somebody     ]   │
│     Genre    [ Pop                            ]   │
│     Year     [ 1987                           ]   │
│                                                   │
│                 [ Save Changes ]                  │
│                                                   │
└───────────────────────────────────────────────────┘

The important architectural idea is:

flowchart TB UI["Flutter UI"] API["hAudiotagger API"] RUST["Rust Engine"] META["Audio Metadata"] FILE["Audio File"] UI --> API API --> RUST RUST --> META META --> FILE

This separation makes the application much easier to maintain.


Why Flutter?

Flutter is a particularly interesting choice for this kind of application because the same application code can target mobile, desktop, and Web.

Flutter officially supports native Windows, macOS, and Linux desktop applications, while Web support allows Flutter applications to run in modern browsers.

That means we can build a music tag editor without writing:

Android implementation
iOS implementation
Windows implementation
macOS implementation
Linux implementation
Web implementation

separately.

Instead, our UI can remain mostly:

Flutter
   ↓
Dart

while the metadata implementation is handled by a dedicated library.


Why not implement everything in Dart?

You absolutely could.

But audio metadata is deceptively complicated.

Different formats have different metadata systems.

For example:

MP3
 ├── ID3v1
 ├── ID3v2
 └── APE

FLAC
 └── Vorbis Comments

MP4 / M4A
 └── iTunes-style atoms

WAV
 ├── ID3
 └── RIFF INFO

OGG / Opus
 └── Vorbis Comments

So instead of rebuilding an entire metadata engine inside the Flutter application, we'll use a library designed specifically for this job.

That's where hAudiotagger comes in.


Introducing hAudiotagger

hAudiotagger is a Rust-powered audio metadata library for Flutter.

The current 2.0.5 release supports Android, iOS, Linux, macOS, Windows, and Web, with support for formats including MP3, FLAC, OGG/Opus, MP4/M4A, WAV, AIFF, APE and WavPack.

The package uses Rust underneath and exposes the functionality to Dart through flutter_rust_bridge.

Conceptually:

flowchart TB DART["Flutter / Dart"] BRIDGE["flutter_rust_bridge"] RUST["Rust"] ENGINE["Audio Metadata Engine"] DART -->|"API calls"| BRIDGE BRIDGE --> RUST RUST --> ENGINE

flutter_rust_bridge is designed to let Flutter/Dart call Rust code through generated bindings, including asynchronous Rust APIs and richer Rust/Dart types.

The important part for us is that we don't need to think about the Rust layer while building our UI.

We simply call a Dart API.


Step 1: Create the Flutter project

Let's start with a normal Flutter application:

flutter create music_tag_editor
cd music_tag_editor

Check that Flutter is working:

flutter doctor

Then run the application:

flutter run

At this point, you should have a completely normal Flutter application.

Nothing fancy yet.


Step 2: Add hAudiotagger

Add the package:

flutter pub add haudiotagger

Or manually add it to pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter

  haudiotagger: ^2.0.5

Then:

flutter pub get

That's it.

The metadata engine is now available to our Flutter application.


Step 3: Read metadata from a file

Let's start with the simplest possible operation.

import 'package:haudiotagger/haudiotagger.dart';

final tag = await Haudiotagger.read('/path/to/song.mp3');

print(tag?.title);
print(tag?.artist);
print(tag?.album);

This gives us access to the metadata stored inside the file.

For example:

Title:   Never Gonna Give You Up
Artist:  Rick Astley
Album:   Whenever You Need Somebody

The important thing here is that we're not parsing ID3 frames ourselves.

We're working with a higher-level Dart API.


Step 4: Select an audio file

A tag editor isn't very useful if the user has to manually type:

/home/hirdaya/Music/song.mp3

into a text field.

Let's give the user a file picker.

One possible implementation is:

flutter pub add file_picker

Then:

import 'package:file_picker/file_picker.dart';

Future<String?> pickAudioFile() async {
  final result = await FilePicker.pickFile(
    type: FileType.custom,
    allowedExtensions: [
      'mp3',
      'flac',
      'ogg',
      'opus',
      'm4a',
      'mp4',
      'wav',
      'aiff',
      'ape',
      'wv',
    ],
  );

  if (result == null) {
    return null;
  }

  return result.path;
}

Now our flow becomes:

flowchart TD USER["User taps Open File"] PICKER["File Picker"] FILE["Audio File Selected"] READ["hAudiotagger reads metadata"] UI["Flutter displays metadata"] USER --> PICKER PICKER --> FILE FILE --> READ READ --> UI

Step 5: Load the metadata into the form

When a file is selected:

Future<void> loadMetadata(String path) async {
  final tag = await Haudiotagger.read(path);

  if (tag == null) {
    return;
  }

  titleController.text = tag.title ?? '';
  artistController.text = tag.artist ?? '';
  albumController.text = tag.album ?? '';
  genreController.text = tag.genre ?? '';

  if (tag.year != null) {
    yearController.text = tag.year.toString();
  }
}

Now the user can select:

song.mp3

and the UI automatically becomes:

Title    [ Never Gonna Give You Up ]
Artist   [ Rick Astley             ]
Album    [ Whenever You Need...    ]
Genre    [ Pop                     ]
Year     [ 1987                    ]

That's already a useful application.


Step 6: Write metadata

Now comes the fun part.

When the user taps Save, we need to write the metadata back into the audio file.

Future<void> saveMetadata(String path) async {
  await Haudiotagger.write(
    path,
    Tag(
      title: titleController.text,
      artist: artistController.text,
      album: albumController.text,
      genre: genreController.text,
      year: int.tryParse(yearController.text),
    ),
  );
}

And that's our first complete read/write workflow.

flowchart LR OPEN["Open"] READ["Read Metadata"] DISPLAY["Display"] EDIT["Edit"] SAVE["Save"] WRITE["Write Metadata"] OPEN --> READ READ --> DISPLAY DISPLAY --> EDIT EDIT --> SAVE SAVE --> WRITE

Step 7: Don't overwrite everything unnecessarily

There is an important difference between:

write(...)

and:

update(...)

Suppose the file contains:

Title
Artist
Album
Genre
Year
Lyrics
Cover Art
Composer
Copyright
Custom Tags

but the user only wants to change:

Genre → Rock

You don't want your application to accidentally reconstruct the entire tag and lose unrelated metadata.

Instead, use a partial update.

await Haudiotagger.update(
  path,
  TagChanges(
    genre: 'Rock',
  ),
);

The update operation changes only the fields supplied in TagChanges, preserving the other fields.

This is a much safer pattern for an editor.


#$ Step 8: Add album artwork

Music metadata isn't just text.

A proper music library also needs album artwork.

hAudiotagger supports reading and writing embedded cover art.

Conceptually, our UI can look like:

┌─────────────────────┐
│                     │
│     ALBUM ART       │
│                     │
│      500×500        │
│                     │
└─────────────────────┘

[ Change Artwork ]

When the user selects an image, we can update the embedded artwork instead of storing the image separately.

This matters because the artwork then travels with the audio file.

Move the file to another computer:

song.mp3

and the artwork is still there.


Step 9: Support bytes for Web

Here's where things get particularly interesting.

On native platforms, working with a file path is convenient:

Haudiotagger.read(path);

But browsers have a different security model.

A Web application generally doesn't get unrestricted access to arbitrary files on the user's filesystem.

Instead, users select files through browser APIs.

That means a byte-oriented API becomes extremely useful:

final tag = await Haudiotagger.readFromBytes(fileBytes);

And after modification:

final modified = await Haudiotagger.writeToBytes(
  fileBytes,
  tag,
);

hAudiotagger exposes byte-based APIs alongside file-path APIs for this reason.

This gives us a nice architecture:

flowchart TB subgraph NATIVE["Native Platforms"] NF["Audio File"] NP["File Path"] NH["hAudiotagger"] NF --> NP NP --> NH end subgraph WEB["Web"] WF["Browser File"] WB["File Bytes"] WH["hAudiotagger"] WM["Modified Bytes"] WD["Browser Download"] WF --> WB WB --> WH WH --> WM WM --> WD end

Step 10: Understanding the Web architecture

The Web implementation is particularly interesting because hAudiotagger uses a WebAssembly-compiled Rust implementation.

The package documentation describes the Web implementation as using a WASM Rust binary and Web Workers for non-blocking calls.

The architecture therefore looks roughly like:

flowchart TB FLUTTER["Flutter Web"] DART["Dart API"] WASM["WebAssembly"] RUST["Rust"] META["Audio Metadata"] FLUTTER --> DART DART --> WASM WASM --> RUST RUST --> META

Flutter itself supports WebAssembly as part of its Web platform capabilities.

This is one of the reasons a metadata editor can be built as a browser application rather than requiring users to install a desktop program.


Step 11: Add technical audio information

Metadata is only half of the story.

A good editor can also show technical information:

Duration       04:32
Bitrate        320 kbps
Sample Rate    44.1 kHz
Channels       Stereo
Codec          MP3
Container      MPEG Audio
Lossless       No

hAudiotagger exposes audio properties such as duration, bitrate, sample rate, channels, bits per sample, codec, container, and lossless status.

This lets us build a useful information panel:

TECHNICAL INFORMATION

Codec             MP3
Bitrate           320 kbps
Sample Rate       44.1 kHz
Channels          2
Duration          04:32

Now our application is becoming more than a simple form.

It's an actual audio inspector.


Step 12: What makes this project interesting?

At this point, we have a useful application.

But there's a bigger lesson here.

The application isn't interesting merely because it can edit:

Title
Artist
Album

There are many applications that can do that.

What's interesting is the architecture:

flowchart TB FLUTTER["Flutter"] RUST["Rust"] CROSS["Cross-platform"] WASM["WebAssembly"] NATIVE["Native Binary"] FLUTTER --> RUST FLUTTER --> CROSS RUST --> NATIVE RUST --> WASM

This combination lets us build an application that can target:

flowchart TB FLUTTER["Shared Flutter Codebase"] FLUTTER --> ANDROID["Android"] FLUTTER --> IOS["iOS"] FLUTTER --> WINDOWS["Windows"] FLUTTER --> MACOS["macOS"] FLUTTER --> LINUX["Linux"] FLUTTER --> WEB["Web"]

from a shared Flutter codebase.

That's the real story.


The architecture in one diagram

Our finished application looks roughly like this:

flowchart TB APP["Music Tag Editor"] UI["Flutter UI"] SERVICES["Application Services"] API["hAudiotagger<br/>Dart API"] BRIDGE["flutter_rust_bridge"] RUST["Rust Metadata Layer"] FORMATS["Audio Format Support"] APP --> UI UI --> SERVICES SERVICES --> API API --> BRIDGE BRIDGE --> RUST RUST --> FORMATS

For Web:

flowchart LR FLUTTER["Flutter Web"] DART["Dart"] WASM["WebAssembly"] RUST["Rust"] BYTES["Audio Bytes"] FLUTTER --> DART DART --> WASM WASM --> RUST RUST --> BYTES

For native platforms:

flowchart LR FLUTTER["Flutter"] DART["Dart"] RUST["Rust Native Library"] FILE["Audio File"] FLUTTER --> DART DART --> RUST RUST --> FILE

What I learned building this

There are a few lessons worth taking away.

Cross-platform doesn't mean identical behavior

A file path makes sense on desktop.

A browser-selected byte buffer makes more sense on Web.

The API should abstract the metadata operation without pretending that every platform works exactly the same way.


Metadata is messier than it looks

The happy path is:

Read
→ Edit
→ Write

The real world is:

Read
→ Detect format
→ Parse
→ Handle missing fields
→ Preserve unrelated metadata
→ Handle artwork
→ Handle encoding
→ Validate
→ Write
→ Verify

That's where the engineering gets interesting.


Final result

We started with:

A Flutter application

and ended with:

Cross-platform music metadata editor

with:

  • Dart application logic
  • Rust-powered metadata processing
  • Native platform support
  • WebAssembly support
  • Multiple audio formats
  • Metadata editing
  • Partial updates
  • Album artwork
  • Technical audio information

The basic editor is only the beginning.

The same architecture can power a:

Music tag editor
Music library manager
Audio organizer
Metadata cleaner
Album artwork manager
Podcast editor
Audiobook organizer
Audio inspection tool

And that's the interesting part of building developer tools: once the underlying engine is solid, the possibilities multiply.


Try it yourself

If you want to experiment with the metadata layer directly, install hAudiotagger:

dependencies:
  haudiotagger: ^2.0.5

Then:

import 'package:haudiotagger/haudiotagger.dart';

final tag = await Haudiotagger.read('/path/to/song.mp3');

print(tag?.title);
print(tag?.artist);
print(tag?.album);

For more advanced workflows, explore the package documentation and source code.

Package: pub.dev

Source: Github


What's next?

A simple tag editor is useful.

A complete music library tool is something else entirely.

The next logical step is to build:

A music library cleaner that can scan thousands of files, detect inconsistent metadata, preview the changes, and automatically normalize the collection.

That's where audio metadata becomes really interesting.

And that's exactly where hAudiotagger starts to shine ✨️


Found this useful

If you're building a Flutter application that needs to read or edit audio metadata, check out hAudiotagger on pub.dev.

If you build something with it, I'd genuinely love to see what you make.

⭐ Star the project on GitHub, open an issue if you find something weird, and share your use case.

The best developer tools aren't built in isolation.

They're built by people using them, breaking them, complaining about them, fixing them, and making them better.

Happy Shattering...