
Xeno-Canto annotations
suwo: access nature media repositories
Marcelo Araya-Salas
2026-10-07
Source:vignettes/xenocanto_annotations.Rmd
xenocanto_annotations.RmdThe goal of this vignette is to illustrate how to query for and retrieve Xeno-Canto annotation data using query_xenocanto()
Xeno-Canto recordings are sometimes annotated: one or more sounds in the sound file are individually marked with their own species identification, time and frequency bounds, sound type, and other details.suwo (version >= 0.2.3) now allows users to retrieved annotations along with the associated sound files.
Basic usage
query_xenocanto()
requires a Xeno-Canto API key. Get yours at https://xeno-canto.org/account,
then set it as an environment variable named
xc_api_key:
# run this in the console, don't save it in a script
Sys.setenv(xc_api_key = "YOUR_API_KEY_HERE")A regular call to query_xenocanto() works exactly as usual:
pippip <- query_xenocanto(species = "Pipistrellus pipistrellus")ℹ Obtaining metadata:
✔ 1154 matching sound files found 🎊
✔ 2 annotations found and added to the returned object's `annotations` attribute (access with attr(<your_object>, "annotations")) 🌈
The main result is the same data frame returned by any of the suwo’s query function, one row per recording:
## key species country
## 1 1165950 Pipistrellus pipistrellus Poland
## 2 1164494 Pipistrellus pipistrellus Netherlands
## 3 1154947 Pipistrellus pipistrellus Netherlands
## 4 1154946 Pipistrellus pipistrellus Netherlands
## locality
## 1 Gmina Pabianice (near Pabianice), Powiat pabianicki, Łódzkie
## 2 Zoetermeer, Meerzicht (near Zoetermeer), Zoetermeer, Zuid-Holland
## 3 Cadier en Keer, Kloosterpad (near Cadier en Keer), Eijsden-Margraten, Limburg
## 4 Cadier en Keer, Kloosterpad (near Cadier en Keer), Eijsden-Margraten, Limburg
Whenever any of the matching recordings have annotations attached, a
second message reports how many were found and confirms they were added
to the result as an attribute. If none of the matching recordings have
annotations, this message is simply not shown, and the attribute is
NULL.
Accessing the annotations
Retrieve the annotations with attr():
## [1] 2
Each row is one annotated segment, not one recording. A single recording can have several annotations, each describing a different part of that one sound file:
annotations[, c("key", "species", "start_time", "end_time", "frequency_low", "frequency_high")]## key species start_time end_time frequency_low
## 1 1040808 Pipistrellus pipistrellus 0.010345 6.05997 14719
## 2 1040808 Pipistrellus pipistrellus 2.580910 4.60631 45469
## frequency_high
## 1 48281
## 2 66281
Understanding the annotation columns
The full set of columns returned includes:
names(annotations)## [1] "annotation_xc_id" "key"
## [3] "species" "annotator"
## [5] "start_time" "end_time"
## [7] "frequency_high" "frequency_low"
## [9] "sound_type" "sex"
## [11] "life_stage" "annotation_remarks"
## [13] "original_set_name" "original_set_creator"
## [15] "original_set_owner" "original_set_license"
## [17] "original_set_uri" "original_set_creation_date"
## [19] "annotation_set_name" "annotation_set_creator"
## [21] "annotation_set_creation_date" "annotation_set_remarks"
## [23] "file_url" "observation_url"
A few worth highlighting:
-
key. The Xeno-Canto ID of the recording this annotation belongs to. This matches thekeycolumn of the main result, and is the column to join on (see below). -
species. The species identified in this specific annotated segment. This is usually, but not necessarily always, the same as the parent recording’s overall species. It can differ for an annotated background call of a different species. -
start_time/end_time. The annotated segment’s time bounds within the recording, in seconds. -
frequency_low/frequency_high. The annotated segment’s frequency bounds, in Hz. -
annotator. Who created this specific annotation. -
sound_type,sex,life_stage,annotation_remarks. Further details about the annotated segment, when available. -
annotation_set_name,annotation_set_creator,annotation_set_creation_date,annotation_set_remarks. Metadata about the set of annotations this one belongs to (a recording’s annotations are usually created together, as one set). -
file_url/observation_url. Links back to the parent recording.
Since several annotation rows commonly share the same
key (several segments from the same recording), values in
key and species are expected to repeat. This
reflects the data correctly rather than indicating a problem.
Searching specifically for annotated recordings
Annotation data is available on any query’s result whenever present,
regardless of the search used. To instead search specifically for
recordings that have an annotation matching certain criteria, use
Xeno-Canto’s ann_* search tags directly in the
species argument (e.g. ann_sp for the
annotated species, ann_type for the annotated sound
type):
# recordings with an annotation identified as this species
corcor <- query_xenocanto(species = 'ann_sp:"Corvus corax"')ℹ Obtaining metadata:
✔ 153 matching sound files found 🎊
✔ 491 annotations found and added to the returned object's `annotations` attribute (access with attr(<your_object>, "annotations")) 😸
This only changes which recordings are returned by the
search, but does not change how annotation data itself is retrieved or
structured. The annotations attribute is extracted the same
way either way.
Landscape recordings
Some annotations come from soundscape/landscape
recordings rather than a recording of a single focal species.
Xeno-Canto labels the species in these cases as
Sonus naturalis (not a real biological taxon, but a
placeholder Xeno-Canto uses specifically for this recording type). This
can show up unexpectedly when inspecting annotation species, even for a
search targeting a real species:
table(corcor$species)##
## Corvus corax Sonus naturalis
## 2 151
Keep this in mind when filtering or summarizing annotations by
species. To search specifically for this kind of recording, use
grp:soundscape (or grp:0). See Searching non-bird taxa below.
Searching non-bird taxa
The grp: tag narrows a search to a specific taxonomic
group, and is especially useful combined with other tags. Valid values
are grp:birds, grp:grasshoppers,
grp:bats, grp:frogs, and
grp:"land mammals" (note the quotes, needed because the
value contains a space). Numeric IDs work too (1 through
5 for the five groups above, e.g. grp:2 is
equivalent to grp:grasshoppers). Soundscape recordings are
a special case, since they can include multiple groups at once. Use
grp:soundscape or grp:0 to search those
specifically.
Including grp: matters in particular when combined with
ann_* annotation tags: omitting it for a non-bird species
alongside an annotation tag can cause the request to fail outright with
a server error, rather than simply returning no results:
# fails with an HTTP 500 server error (grasshoppers require grp: to be
# specified explicitly when searching by annotation tags)
query_xenocanto(species = 'ann_sp:"Gryllus campestris"')Including grp: resolves it:
grycamp <- query_xenocanto(
species = 'grp:grasshoppers ann_sp:"Gryllus campestris"'
)ℹ Obtaining metadata:
✔ 4 matching sound files found 🎊
✔ 10 annotations found and added to the returned object's `annotations` attribute (access with attr(<your_object>, "annotations")) 🌈
The ann:yes tag searches for recordings that have
any annotation attached, regardless of what species or type it
identifies. This is unlike the ann_* tags
(ann_sp, ann_type, etc.), which filter by a
specific annotation attribute. By default this will return only bird
recordings, since Xeno-Canto is primarily a bird sound repository.
all_birds <- query_xenocanto(species = "ann:yes")To search for annotated recordings in other groups it must be
combined with grp::
all_bats <- query_xenocanto(species = "ann:yes grp:bats")
all_frogs <- query_xenocanto(species = "ann:yes grp:frogs")For more details on Xeno-Canto’s annotation system, see their official documentation.
Plugging annotations into acoustic analysis workflows
Annotations can then be easily formatted to be used by R packages that work with annotated acoustic data (e.g. warbleR) for further manipulation and analysis. The following function formats annotations to meet warbleR’s requirements. It takes the full output of query_xenocanto() and returns a data frame with the columns required by warbleR:
anns_warbler <- function(X) {
Y <- attr(X, "annotations")
X$main.species <- X$species
Y <- merge(
Y,
X[, c("key", "file_extension", "main.species")],
by = "key",
all.x = TRUE,
all.y = FALSE
)
Y$sound.files <-
paste0(
gsub(pattern = " ", "_", x = Y$main.species),
"-",
"XC",
Y$key,
".",
Y$file_extension
)
names(Y)[names(Y) == "start_time"] <- "start"
names(Y)[names(Y) == "end_time"] <- "end"
names(Y)[names(Y) == "frequency_low"] <- "bottom.freq"
names(Y)[names(Y) == "frequency_high"] <- "top.freq"
Y$bottom.freq <- Y$bottom.freq / 1000
Y$top.freq <- Y$top.freq / 1000
return(Y)
}
# Example usage of the anns_warbler function
head(anns_warbler(grycamp), 2)## key annotation_xc_id species annotator start end
## 1 1104794 1488 Gryllus campestris Burooj Ghani 0.0160 1.1338
## 2 1104794 1489 Gryllus campestris Burooj Ghani 1.4904 4.3828
## top.freq bottom.freq sound_type sex life_stage annotation_remarks
## 1 15.639 3.524 <NA> <NA> <NA> NA
## 2 16.147 4.167 <NA> <NA> <NA> NA
## original_set_name original_set_creator original_set_owner
## 1 Annotation set for 1104794 Burooj Ghani <NA>
## 2 Annotation set for 1104794 Burooj Ghani <NA>
## original_set_license original_set_uri
## 1 CC-BY-4.0 https://xeno-canto.org/annotation/set/102
## 2 CC-BY-4.0 https://xeno-canto.org/annotation/set/102
## original_set_creation_date annotation_set_name
## 1 2026-05-02 23:52:40 Annotation set for XC1104794
## 2 2026-05-02 23:52:40 Annotation set for XC1104794
## annotation_set_creator annotation_set_creation_date
## 1 <NA> 2026-09-10 17:21:00
## 2 <NA> 2026-09-10 17:21:00
## annotation_set_remarks
## 1 Metadata of the original set is provided for each individual annotation
## 2 Metadata of the original set is provided for each individual annotation
## file_url observation_url
## 1 https://xeno-canto.org/1104794/download https://xeno-canto.org/1104794
## 2 https://xeno-canto.org/1104794/download https://xeno-canto.org/1104794
## file_extension main.species sound.files
## 1 wav Gryllus campestris Gryllus_campestris-XC1104794.wav
## 2 wav Gryllus campestris Gryllus_campestris-XC1104794.wav
These formatted annotations can be used with warbleR, baRulho, or ohun functions to visualize and analyze the annotated segments. Alternatively, they can be exported to Raven Pro software using Rraven for further manipulation. In either case, the corresponding sound files must first be downloaded using suwo’s download_media().
Session information
Click to see
## R version 4.6.0 (2026-04-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.4 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
## [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] suwo_0.2.2
##
## loaded via a namespace (and not attached):
## [1] digest_0.6.39 desc_1.4.3 R6_2.6.1 fastmap_1.2.0
## [5] xfun_0.61 cachem_1.1.0 knitr_1.52 htmltools_0.5.9
## [9] rmarkdown_2.32 lifecycle_1.0.5 cli_3.6.6 sass_0.4.10
## [13] pkgdown_2.2.1 textshaping_1.0.5 jquerylib_0.1.4 systemfonts_1.3.2
## [17] compiler_4.6.0 tools_4.6.0 ragg_1.5.2 bslib_0.12.0
## [21] evaluate_1.0.5 yaml_2.3.12 otel_0.2.0 jsonlite_2.0.0
## [25] rlang_1.3.0 fs_2.1.0 htmlwidgets_1.6.4