monatone
Pure Haskell library for audio metadata parsing and writing
https://github.com/rembo10/monatone
| Stackage Nightly 2026-07-30: | 0.4.0.1 |
| Latest on Hackage: | 0.4.0.1 |
GPL-3.0-only licensed by rembo10
Maintained by [email protected]
This version can be pinned in stack with:
monatone-0.4.0.1@sha256:e1095298f0439e7ab950fa88efb0615aaa8719c28e63c6cbc3ff7fab4c270938,5407Module documentation for 0.4.0.1
Depends on 16 packages(full list with versions):
Used by 1 package in nightly-2026-07-30(full list with versions):
Monatone
A pure Haskell library for parsing and writing audio metadata.
Features
- Pure Haskell - No FFI dependencies or external binaries required
- Multiple format support - FLAC, MP3 (ID3v1/v2), OGG/Vorbis, and Opus
- Read and write - Full tag writing support for FLAC and MP3
- Type-safe - Strongly typed metadata representation
- Efficient - Streaming support for large files
Installation
cabal install monatone
Or add to your .cabal file:
build-depends: monatone >= 0.1.0
Quick Start
import Monatone.FLAC (parseMetadata)
import Monatone.Metadata
-- Parse metadata from a FLAC file
main :: IO ()
main = do
result <- parseMetadata "song.flac"
case result of
Left err -> putStrLn $ "Error: " ++ show err
Right metadata -> do
putStrLn $ "Title: " ++ maybe "Unknown" id (title metadata)
putStrLn $ "Artist: " ++ maybe "Unknown" id (artist metadata)
putStrLn $ "Album: " ++ maybe "Unknown" id (album metadata)
Supported Formats
FLAC
- Full Vorbis comment support (read/write)
- PICTURE block support for album art
- StreamInfo parsing for audio properties
MP3
- ID3v1 tag support (read/write)
- ID3v2.3 and ID3v2.4 support (read/write)
- APIC frame support for album art
- Common frames: TIT2, TPE1, TALB, TYER, TCON, etc.
OGG/Vorbis
- Vorbis comment reading
- Basic metadata extraction
Opus
- Basic metadata support through Vorbis comments
Writing Tags
import Monatone.Writer
import Monatone.Types
import qualified Data.Map as Map
-- Update MP3 tags
updateMP3Tags :: FilePath -> IO ()
updateMP3Tags file = do
let tags = Map.fromList
[ ("TIT2", "New Title")
, ("TPE1", "New Artist")
, ("TALB", "New Album")
]
result <- writeMP3Tags file tags
case result of
Left err -> putStrLn $ "Error: " ++ err
Right () -> putStrLn "Tags updated successfully"
API Documentation
Full API documentation is available on Hackage.
Contributing
Contributions are welcome! Please feel free to submit issues and pull requests on GitHub.
License
GPL 3.0 License - see LICENSE file for details.
Changes
Changelog for monatone
[0.4.0.1] - 2026-07-25
Fixed
- M4A updates carry unknown typed and binary ilst atoms (tmpo, cpil,
binary freeform data) over verbatim instead of dropping them; text
tags keep round-tripping through
rawTagsso edits there still win - FLAC files with an ID3v2 tag prepended by other tools are detected as FLAC rather than MP3, parse correctly, and updates strip the stale tag
- CI: a checked-in
cabal.projectwithtests: Trueforces the solver to include the test suite, fixingcabal testfailures on CI
[0.4.0.0] - 2026-07-24
Added
- OGG/Opus tag writing: Vorbis and Opus files can now be updated like every other format. The comment packet is rebuilt and repaginated, following pages are renumbered and their CRCs recomputed, and audio data is copied verbatim; the encoder’s vendor string is preserved. Album art travels as METADATA_BLOCK_PICTURE comments (kept, set, or removed like the other formats). Verified against ffmpeg-generated files (tags readable by ffprobe, streams decode cleanly).
- OGG/Opus duration is computed from the final page’s granule position (accounting for Opus pre-skip)
- Writes are durable: the temp file is fsynced before the atomic rename (unix only), so a power loss cannot leave a half-written file
- Multiple album art pictures survive updates: writers take an explicit art instruction (keep / set / remove), and “keep” carries all existing FLAC PICTURE blocks, MP3 APIC frames, and M4A covr atoms through verbatim
- MP3 binary frames (USLT lyrics, PRIV, GEOB, SYLT, POPM, URL frames, foreign UFIDs) are carried through updates verbatim; unmapped ID3v2.2 text frames are translated to their v2.4 equivalents
- The FLAC writer preserves the original encoder’s vendor string
- ID3v1/Winamp genre table: ID3v2.3 “(nn)” references and ID3v2.4 bare numeric genre strings resolve to genre names, and the ID3v1 genre byte is read as a fallback
Changed
- Breaking:
writeMetadataand the format writers take anAlbumArtUpdate(KeepExistingArt/SetAlbumArt/RemoveAlbumArt) instead ofMaybe AlbumArt, whoseNothingconflated “no change” with “remove” updateMetadatadetects the format from file content instead of the extension, and no longer reloads album art through the parser (whose failure used to silently delete artwork)- Breaking:
rawTagsis nowHashMap Text [Text]so multi-valued tags are represented faithfully. Scalar metadata fields (artist,genre, …) expose the first value. On write, if a scalar field still matches the first stored value the whole list is written back; editing the field replaces the list.
Fixed
- ALAC audio properties (bit depth, sample rate, channels) are read from the alac extension box; the extension-parsing branch was unreachable, so 24-bit files reported the sample-entry header’s 16-bit claim, and the (dead) alac parser read the wrong offset for the sample rate
- MP3 UFID frames are parsed properly (owner + identifier) instead of being garbled by the text-frame parser; the MusicBrainz recording id is read from and written to UFID:http://musicbrainz.org per Picard convention (previously mapped to the wrong field and never matched)
- An existing ID3v1 tag is rewritten on update instead of keeping stale pre-update fields visible to v1-only readers
- ID3v2 header flags are now honoured: whole-tag unsynchronisation is reversed, the extended header is skipped (v2.3 and v2.4 layouts), and the v2.4 footer is accounted for when locating audio data; v2.4 per-frame unsynchronisation and data-length indicators are handled too
- UTF-16 COMM/TXXX/USLT frames with descriptions decode correctly: the terminator search is now aligned to UTF-16 code units
- Xing/Info VBR headers are found in MPEG2/2.5 files (side-info offsets differ from MPEG1), fixing missing duration/bitrate for MPEG2 VBR
- ID3v1 fields decode as Latin-1 per spec instead of UTF-8; plain v1.0 tags no longer lose the last 2 comment characters
- An MP3 frame sync spanning a 4096-byte scan-chunk boundary is no longer missed
- The MP3 writer replaces an existing tag’s footer instead of leaving 10 bytes of it before the audio
- Multi-valued tags (e.g. several ARTIST Vorbis comments) are no longer silently collapsed to one value
- Null-separated values in ID3v2.4 text frames are parsed as separate values instead of being concatenated into one string, and the MP3 writer emits multi-valued frames null-separated per ID3v2.4 (one frame per ID, since duplicate frame IDs are invalid)
- Vorbis bitrate fields are read as signed, so the -1 “unset” sentinel is no longer reported as a ~4 Gbps bitrate
- Opus sample rate is reported as 48kHz (the decode rate) rather than the encoder’s informational input rate
- FLAC album art loading continues past a corrupt Picture block to a later one instead of giving up
- Mixed-case file extensions (.Mp3, .Flac) are accepted for updates
- M4A disk atom is written with the 6-byte iTunes layout (trkn keeps its trailing reserved word); track/disc numbers are clamped to 16 bits instead of silently wrapping
- Malformed-input hardening: crafted or truncated files can no longer
hang or crash the parsers and writers
- M4A atoms with sizes smaller than their own header (including zero 64-bit sizes) no longer cause infinite loops in the parser or writer
- Truncated FLAC blocks and lying length fields inside Vorbis comments return clean parse results instead of throwing from pure code
- An MP3 sync pattern with fewer than 4 bytes before EOF no longer crashes the frame-header parser; truncated Xing headers are read safely
- M4A tag payload reads are clamped to the file size, so a lying atom size cannot trigger a huge allocation
- FLAC writer errors out cleanly on truncated source blocks and on metadata blocks exceeding the 16MiB block-length limit instead of writing a corrupt stream
- MP3 writer rejects content beyond ID3v2’s 256MiB syncsafe size limit instead of silently truncating the size field
- OGG packets are now reassembled across pages: comment packets larger than one page (~64KB, i.e. any file with embedded album art) were previously truncated and all tags silently dropped
- Opus tags (
OpusTagspacket) are now parsed; previously Opus files returned no tags and the parser scanned the entire file - OGG album art with base64
=padding no longer fails to decode - OGG parser now populates
rawTagsand stops reading after the two metadata packets
[0.3.0.0] - 2026-07-20
Fixed
- M4A writer corrupted moov-first (“fast start”) files: stco/co64 chunk offsets are now adjusted when the moov atom changes size, so audio stays playable after a tag update
- M4A writer no longer silently discards errors from the file rewrite
- Writes are now atomic: metadata is written to a temporary sibling file which is renamed over the original, so a failed write or crash can no longer corrupt or truncate the audio file
- Updates no longer drop tags the writer does not map: MusicBrainz IDs and AcoustID tags (MP3), track/disc totals (MP3, FLAC), release status/type (M4A), unmapped ID3 text/TXXX frames, Vorbis comments, and iTunes text/freeform atoms are all preserved across updates
- FLAC updates preserve SEEKTABLE, APPLICATION, CUESHEET and unknown metadata blocks instead of discarding them
- Single date field per file:
setYearno longer produces duplicate TDRC frames (MP3) or DATE comments (FLAC) - FLAC comment values containing
=are no longer truncated - MP3 parser now reads disc number and track/disc totals from TRCK/TPOS
- FLAC parser now populates
rawTags; M4A parser now reads release status/type - FLAC vendor string now tracks the package version automatically
Changed
- Breaking: removed
writeMetadataToFilefromMonatone.Writer; usewriteMetadata, which is now atomic and no longer needs the.backupmechanism
[0.2.1.1] - 2025-12-10
Fixed
- Added *.m4a to extra-source-files to include M4A test fixture in sdist tarball
[0.2.1.0] - 2025-12-10
Fixed
- Fixed Hackage description formatting to display properly with multiline text
- Escaped forward slashes in format names to prevent markdown italicization
- Fixed M4A round-trip test to properly skip when fixture unavailable
- Checked in test fixtures (MP3, FLAC) to support Nix builds without ffmpeg
Changed
- Tests no longer require ffmpeg at build time (only for regenerating fixtures)
[0.2.0.0] - 2025-12-10
Added
- M4A/AAC/ALAC Support: Complete read/write support for M4A audio files
- Full metadata parsing with iTunes-style tags (©nam, ©ART, aART, etc.)
- Audio properties extraction (duration, sample rate, channels, bit depth)
- AAC and Apple Lossless (ALAC) codec detection and parsing
- Album art extraction (JPEG, PNG, BMP)
- Track/disc number parsing with iTunes binary format
- Complete metadata writing with atom reconstruction
- Freeform atom support for MusicBrainz-style fields:
- Record label (LABEL)
- Catalog number (CATALOGNUMBER)
- Barcode (BARCODE)
- Release country (MusicBrainz Album Release Country)
- All MusicBrainz IDs (recording, release, artist, album artist, release group, work, disc)
- Acoustid fingerprint and ID
- Integration with unified Writer API
- Comprehensive test coverage
Technical Details
- Pure Haskell MP4 atom parser with hierarchical structure support
- Efficient streaming implementation (no full file loading)
- Support for both 32-bit and 64-bit atom sizes
- Version-aware parsing (mvhd v0/v1, etc.)
- Safe file writing with temporary file approach
- Freeform (––) atom parsing and writing for custom metadata
- Zero compiler warnings
[0.1.0.0] - Initial Release
Added
- FLAC read/write support with Vorbis comments
- MP3 read/write support with ID3v1, ID3v2.3, and ID3v2.4 tags
- OGG/Vorbis and Opus basic support
- Album art handling
- Pure Haskell implementation