diff --git a/crates/audio-io/src/wav.rs b/crates/audio-io/src/wav.rs
--- a/crates/audio-io/src/wav.rs
+++ b/crates/audio-io/src/wav.rs
@@ -342,7 +342,10 @@ pub(crate) fn read_channel_mask(b: &[u8]) -> Option<u32> {
 
 fn put_fixed_str(out: &mut Vec<u8>, s: &str, len: usize) {
     let bytes = s.as_bytes();
-    let n = bytes.len().min(len);
+    let mut n = bytes.len().min(len);
+    while !s.is_char_boundary(n) {
+        n -= 1;
+    }
     out.extend_from_slice(bytes.get(..n).unwrap_or_default());
     out.resize(out.len() + (len - n), 0);
 }
diff --git a/crates/audio-io/tests/it/roundtrip.rs b/crates/audio-io/tests/it/roundtrip.rs
--- a/crates/audio-io/tests/it/roundtrip.rs
+++ b/crates/audio-io/tests/it/roundtrip.rs
@@ -316,3 +316,13 @@ fn float_wav_header_rejects_a_frame_count_whose_total_size_overflows() {
     let frames = u64::MAX / 4;
     assert!(matches!(wav_float_header(1, 48_000, frames), Err(soundcraft_audio_io::AudioError::TooLarge(_))));
 }
+
+#[test]
+fn bwf_truncation_keeps_utf8_boundaries() {
+    let src = sine(48_000, 1, 10);
+    let bwf = BwfInfo { originator: format!("{}é", "a".repeat(31)), description: format!("{}é", "d".repeat(255)), ..BwfInfo::default() };
+    let bytes = encode(&src, &EncodeOptions { format: FileFormat::Wav, bwf: Some(bwf), ..EncodeOptions::default() }).unwrap();
+    let out = probe(&bytes, None).unwrap().bwf.unwrap();
+    assert_eq!(out.originator, "a".repeat(31));
+    assert_eq!(out.description, "d".repeat(255));
+}
