
{"id":2148,"date":"2026-09-27T20:00:00","date_gmt":"2026-09-27T18:00:00","guid":{"rendered":"https:\/\/www.pcjv.de\/?p=2148"},"modified":"2026-10-04T19:08:28","modified_gmt":"2026-10-04T17:08:28","slug":"go-sofa","status":"publish","type":"post","link":"https:\/\/www.pcjv.de\/?p=2148","title":{"rendered":"go-sofa and go-hdf5: how a head hears, in pure Go"},"content":{"rendered":"\n\n\n\n<p class=\"wp-block-paragraph\">A SOFA file describes how a head hears. SOFA (Spatially Oriented Format for Acoustics, standardised as AES69) stores head-related transfer functions: for hundreds or thousands of directions, the impulse response from a loudspeaker at that direction to a microphone in each ear. With such a set you can place any sound anywhere around a listener on headphones. I wrote my first SOFA reader, PasSofa, in Object Pascal in 2017, and ported it to JavaScript as WebSofa. Now there is <a href=\"https:\/\/github.com\/CWBudde\/go-sofa\" target=\"_blank\" rel=\"noopener\">go-sofa<\/a>, which reads and writes SOFA files in pure Go, on top of my fork of a pure-Go HDF5 library, <a href=\"https:\/\/github.com\/CWBudde\/go-hdf5\" target=\"_blank\" rel=\"noopener\">go-hdf5<\/a>. Both started in February. On the last weekend of September they got about 230 commits and 39,000 added lines. Every figure in this post was made by a Go program that reads the files with go-sofa or go-hdf5.<\/p>\n\n\n<!--more-->\n\n\n<h2 class=\"wp-block-heading\">Listen first (on headphones)<\/h2>\n\n\n\n<figure class=\"wp-block-audio\"><audio controls src=\"\/wordpress\/wp-content\/uploads\/sofa-circle.mp3\"><\/audio><figcaption class=\"wp-element-caption\">One sentence from go-pocket-tts, travelling once around your head: front, left, behind, right.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The voice comes from go-pocket-tts, my Go port of Kyutai&#8217;s Pocket TTS. FFmpeg&#8217;s <code>sofalizer<\/code> filter rendered it at 72 directions, using the MIT KEMAR head-related impulse responses, and a few lines of Go crossfade between those renders while the angle turns. The KEMAR file was re-saved by go-sofa first. That detail matters: <code>sofalizer<\/code> loads SOFA files with libmysofa, the C library most players and plugins use, and a file saved by go-sofa v0.2.0, released on 26 September, gets only &#8220;Selected SOFA file is invalid&#8221;. Most of the weekend went into fixing that.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What a SOFA file holds<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A SOFA file is a set of arrays named after their dimensions: M measurements, R receivers (the two ears), E emitters and N samples. <code>Data.IR<\/code> holds the impulse responses as <code>[M][R][N]<\/code>, <code>SourcePosition<\/code> says where the loudspeaker was for each measurement, and global attributes name the convention, such as <code>SimpleFreeFieldHRIR<\/code>, plus the author, licence and the software that wrote the file. Besides impulse responses (FIR), go-sofa reads and writes transfer functions (TF and TF-E, including spherical-harmonics HRTFs) and second-order sections (SOS). Reading a set with go-sofa takes a few lines:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>f, err := sofa.Open(\"MIT_KEMAR_normal_pinna.sofa\")\nif err != nil {\n    log.Fatal(err)\n}\ndefer f.Close()\n\nfmt.Println(f.SOFAConventions, f.M, f.R, f.N) \/\/ SimpleFreeFieldHRIR 710 2 512\nsrc := f.SourcePositions[0]                    \/\/ azimuth, elevation, radius\nir, _ := f.IRAt(0, 0)                          \/\/ first direction, receiver 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">How the directions are chosen depends on who measured them. These are the three databases I used in the figures below:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"\/wordpress\/wp-content\/uploads\/sofa-grids.png\"><img loading=\"lazy\" decoding=\"async\" width=\"2160\" height=\"605\" src=\"\/wordpress\/wp-content\/uploads\/sofa-grids.png\" alt=\"Three scatter plots of measurement directions: regular elevation rings for MIT KEMAR, interaural-polar arcs for CIPIC, a dense regular grid for Mesh2HRTF\" class=\"wp-image-2144\" srcset=\"https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-grids.png 2160w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-grids-300x84.png 300w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-grids-1024x287.png 1024w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-grids-768x215.png 768w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-grids-1536x430.png 1536w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-grids-2048x574.png 2048w\" sizes=\"auto, (max-width: 2160px) 100vw, 2160px\" \/><\/a><figcaption class=\"wp-element-caption\">MIT KEMAR measured a dummy head in rings of constant elevation down to &minus;40\u00b0. CIPIC used a hoop around the listener, which gives arcs around the ear axis. Mesh2HRTF computed a 3D head scan numerically, so it can cover the whole sphere.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Looking at a head<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once the data is in Go, it is just arrays. Here are the 72 horizontal-plane measurements of the KEMAR left ear, one spectrum per direction:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"\/wordpress\/wp-content\/uploads\/sofa-kemar-hplane.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1584\" height=\"792\" src=\"\/wordpress\/wp-content\/uploads\/sofa-kemar-hplane.png\" alt=\"Heatmap of the KEMAR left-ear magnitude response over azimuth and frequency\" class=\"wp-image-2145\" srcset=\"https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-kemar-hplane.png 1584w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-kemar-hplane-300x150.png 300w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-kemar-hplane-1024x512.png 1024w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-kemar-hplane-768x384.png 768w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-kemar-hplane-1536x768.png 1536w\" sizes=\"auto, (max-width: 1584px) 100vw, 1584px\" \/><\/a><figcaption class=\"wp-element-caption\">The ear canal resonance is the bright band at 2\u20133 kHz. The dark line starting at 8 kHz in front and rising to 10 kHz towards the side is a notch from the pinna. Between 200\u00b0 and 340\u00b0 the source is on the right, the head shadows the left ear, and the high frequencies fade away.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The time difference between the ears is the other big cue. I computed it from the low-passed cross-correlation of the two impulse responses, for a dummy head, a human listener and a simulated head, and plotted Woodworth&#8217;s formula for a rigid sphere next to them:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"\/wordpress\/wp-content\/uploads\/sofa-itd.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1584\" height=\"720\" src=\"\/wordpress\/wp-content\/uploads\/sofa-itd.png\" alt=\"Interaural time difference over azimuth for KEMAR, CIPIC subject 003, Mesh2HRTF and a spherical head model\" class=\"wp-image-2146\" srcset=\"https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-itd.png 1584w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-itd-300x136.png 300w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-itd-1024x465.png 1024w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-itd-768x349.png 768w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-itd-1536x698.png 1536w\" sizes=\"auto, (max-width: 1584px) 100vw, 1584px\" \/><\/a><figcaption class=\"wp-element-caption\">The interaural time difference peaks at about 700 \u00b5s for KEMAR and the Mesh2HRTF head and at 800 \u00b5s for CIPIC subject 003. In my first version the CIPIC curve was upside down: the file stores the two ears&#8217; positions swapped, as files from early versions of the ARI SOFA API often do. libmysofa recognises such files by the API name and version in their attributes.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">HDF5 underneath<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Under the hood a SOFA file is a netCDF-4 file, and that is an HDF5 file with some conventions. HDF5 is a small file system of its own, with object headers, B-trees, local, global and fractal heaps, free-space managers, checksums and several versions of most of them. I wrote a small tool against go-hdf5&#8217;s internals that records which structure occupies which bytes, and ran it on the original KEMAR file and on the copy go-sofa v0.3.0 saved:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"\/wordpress\/wp-content\/uploads\/sofa-bytemap-v030.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1584\" height=\"1130\" src=\"\/wordpress\/wp-content\/uploads\/sofa-bytemap-v030.png\" alt=\"Byte maps of the first 36 KiB of two SOFA files, coloured by HDF5 structure\" class=\"wp-image-2151\" srcset=\"https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-bytemap-v030.png 1584w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-bytemap-v030-300x214.png 300w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-bytemap-v030-1024x731.png 1024w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-bytemap-v030-768x548.png 768w, https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/sofa-bytemap-v030-1536x1096.png 1536w\" sizes=\"auto, (max-width: 1584px) 100vw, 1584px\" \/><\/a><figcaption class=\"wp-element-caption\">The same HRTF set written by two libraries. netCDF-C stores every variable in compressed chunks with a B-tree index, and each B-tree node takes 2,616 or 3,136 bytes, even for <code>ListenerPosition<\/code>, which compresses to 11 bytes. go-sofa v0.3.0 writes the small variables uncompressed next to their headers and compresses only the impulse responses, in one chunk per ear, so its file ends up slightly smaller. go-sofa v0.2.0 did not compress at all, and its copy was five times larger.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">go-hdf5 started as a fork of <a href=\"https:\/\/github.com\/scigolib\/hdf5\" target=\"_blank\" rel=\"noopener\">scigolib\/hdf5<\/a>, a pure-Go HDF5 library that reads and writes. In February I added what SOFA needed first, namely attributes on the root group. Writing a file that some reader accepts is easy. Writing one that every reader accepts is the hard part.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What the weekend fixed<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The weekend started with the HDF5 C library itself. On Thursday evening go-hdf5&#8217;s output became readable by libhdf5 and deterministic, byte for byte. Then came dimension scales, which netCDF uses to name the dimensions. With those, h5py, netCDF4-python and <code>ncdump<\/code> open go-sofa&#8217;s files and show <code>M<\/code>, <code>R<\/code> and <code>N<\/code> as real dimensions. My favourite bug of the weekend was in the name index of dense attribute storage. HDF5 hashes the names with Bob Jenkins&#8217; lookup3, which processes 12 bytes at a time, and go-hdf5 handled names whose length is a multiple of 12 differently. In SOFA files, <code>DateModified<\/code> and <code>Organization<\/code> are both exactly 12 characters long, and libhdf5 could not find those two attributes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">libmysofa was harder, because it is not libhdf5. It implements just the part of HDF5 that SOFA files from MATLAB and Python use, and that part has limits. It follows at most 25 header continuation blocks per file. It resolves references only into a global heap that starts below 64 KiB. In dense storage it reads only string attributes, and no strings marked as UTF-8. It also expects the root group to be a new-style group with link messages. go-hdf5 v0.18.0 therefore writes the root group the way netCDF-C does, keeps dataset attributes in the object header, reserves room for each dataset&#8217;s <code>DIMENSION_LIST<\/code> attribute up front, and reserves a global heap collection for the dimension references right after the root group. That is the green block in the byte map. Strings are written in 8 bytes instead of 9, and scalar dataspaces as the 4-byte version-2 message. In go-sofa, <code>ReceiverPosition<\/code> and <code>EmitterPosition<\/code> got the three-dimensional layout the conventions require. Now SimpleFreeFieldHRIR files from go-sofa pass libmysofa&#8217;s <code>mysofa_load<\/code> and <code>mysofa_check<\/code>. CI builds libmysofa at a pinned commit and loads every generated and every re-saved file with it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Along the way go-sofa learned more of the standard: the mandatory attributes and defaults of each convention from the SOFA Toolbox tables, and warnings for files that break a convention. On save it records its own provenance (<code>APIName<\/code>, <code>APIVersion<\/code>, <code>DateModified<\/code>, a <code>History<\/code> line), and it writes reproducible output when <code>SOURCE_DATE_EPOCH<\/code> is set.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Large files and hostile ones<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Some databases are large, so <code>OpenLazy<\/code> reads the metadata and positions and leaves the audio in the file, to be read one measurement at a time. The first test with Kayser2009, eight receivers in 584 directions, took 78 seconds and allocated 75.7 GB. Each measurement touches 32 compressed chunks, more than the chunk cache held, so every chunk was decompressed again for each of the 146 measurements it spans. With a chunk cache sized to what one measurement touches, it now takes 0.6 seconds and 885 MB. In go-hdf5, repeated single-row reads of a compressed dataset dropped from 720 to 13 \u00b5s.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Files are input, so the readers must also survive garbage. go-hdf5 bounds every allocation that comes from a size stored in the file. A test feeds every header message parser every truncated prefix of valid messages, and the fuzzer found one panic in the dataspace parser. go-sofa refuses, before reading any data, a file that declares far more elements than its size makes plausible. Before that, a crafted file of a few kilobytes could make it allocate gigabytes. Every real file I tested stays at least 52 times below that limit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Update, 4 October:<\/strong> go-sofa now compresses when it saves. The audio data is shuffled and deflated at level 4 by default, which brings the re-saved KEMAR file to 1.10 MB, slightly smaller than the 1.17 MB original and down from 5.85 MB. libmysofa had two more surprises. It reads chunk index keys until it finds one carrying the element size, so go-hdf5 v0.18.1 now writes the size into the last key too. And it reads only chunk indexes that fit in a single B-tree node, so go-sofa never splits a variable into more than 64 chunks. <code>WithDeflate(0)<\/code> still writes the old, uncompressed layout.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Trying it<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>go get github.com\/CWBudde\/go-sofa\ngo install github.com\/CWBudde\/go-sofa\/cmd\/sofainfo@latest\nsofainfo MIT_KEMAR_normal_pinna.sofa<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>sofa2json<\/code> dumps a file as JSON, and <code>sofaprobe<\/code> shows the HDF5 structure underneath. The libmysofa fixes and compression are in go-sofa v0.3.0, which needs go-hdf5 v0.18.1. Both are on GitHub: <a href=\"https:\/\/github.com\/CWBudde\/go-sofa\" target=\"_blank\" rel=\"noopener\">go-sofa<\/a> and <a href=\"https:\/\/github.com\/CWBudde\/go-hdf5\" target=\"_blank\" rel=\"noopener\">go-hdf5<\/a>.<\/p>\n\n\n\n\n","protected":false},"excerpt":{"rendered":"<p>A SOFA file describes how a head hears. SOFA (Spatially Oriented Format for Acoustics, standardised as AES69) stores head-related transfer functions: for hundreds or thousands of directions, the impulse response from a loudspeaker at that direction to a microphone in each ear. With such a set you can place any sound anywhere around a listener [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":2169,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,41],"tags":[],"class_list":["post-2148","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dsp","category-go"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.6 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>go-sofa and go-hdf5: how a head hears, in pure Go - Christian&#039;s private site<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.pcjv.de\/?p=2148\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"8 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"go-sofa and go-hdf5: how a head hears, in pure Go\",\"datePublished\":\"2026-09-27T18:00:00+00:00\",\"dateModified\":\"2026-10-04T17:08:28+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148\"},\"wordCount\":1454,\"image\":{\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.pcjv.de\\\/wordpress\\\/wp-content\\\/uploads\\\/teaser-go-sofa.jpg\",\"articleSection\":[\"DSP\",\"Go\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148\",\"url\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148\",\"name\":\"go-sofa and go-hdf5: how a head hears, in pure Go - Christian&#039;s private site\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.pcjv.de\\\/wordpress\\\/wp-content\\\/uploads\\\/teaser-go-sofa.jpg\",\"datePublished\":\"2026-09-27T18:00:00+00:00\",\"dateModified\":\"2026-10-04T17:08:28+00:00\",\"author\":{\"@id\":\"\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.pcjv.de\\\/?p=2148\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148#primaryimage\",\"url\":\"https:\\\/\\\/www.pcjv.de\\\/wordpress\\\/wp-content\\\/uploads\\\/teaser-go-sofa.jpg\",\"contentUrl\":\"https:\\\/\\\/www.pcjv.de\\\/wordpress\\\/wp-content\\\/uploads\\\/teaser-go-sofa.jpg\",\"width\":480,\"height\":360},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/?p=2148#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.pcjv.de\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"go-sofa and go-hdf5: how a head hears, in pure Go\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.pcjv.de\\\/#website\",\"url\":\"https:\\\/\\\/www.pcjv.de\\\/\",\"name\":\"Christian&#039;s private site\",\"description\":\"Private site of Christian-W. Budde\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.pcjv.de\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"go-sofa and go-hdf5: how a head hears, in pure Go - Christian&#039;s private site","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.pcjv.de\/?p=2148","twitter_misc":{"Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.pcjv.de\/?p=2148#article","isPartOf":{"@id":"https:\/\/www.pcjv.de\/?p=2148"},"author":{"name":"","@id":""},"headline":"go-sofa and go-hdf5: how a head hears, in pure Go","datePublished":"2026-09-27T18:00:00+00:00","dateModified":"2026-10-04T17:08:28+00:00","mainEntityOfPage":{"@id":"https:\/\/www.pcjv.de\/?p=2148"},"wordCount":1454,"image":{"@id":"https:\/\/www.pcjv.de\/?p=2148#primaryimage"},"thumbnailUrl":"https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/teaser-go-sofa.jpg","articleSection":["DSP","Go"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/www.pcjv.de\/?p=2148","url":"https:\/\/www.pcjv.de\/?p=2148","name":"go-sofa and go-hdf5: how a head hears, in pure Go - Christian&#039;s private site","isPartOf":{"@id":"https:\/\/www.pcjv.de\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.pcjv.de\/?p=2148#primaryimage"},"image":{"@id":"https:\/\/www.pcjv.de\/?p=2148#primaryimage"},"thumbnailUrl":"https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/teaser-go-sofa.jpg","datePublished":"2026-09-27T18:00:00+00:00","dateModified":"2026-10-04T17:08:28+00:00","author":{"@id":""},"breadcrumb":{"@id":"https:\/\/www.pcjv.de\/?p=2148#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.pcjv.de\/?p=2148"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.pcjv.de\/?p=2148#primaryimage","url":"https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/teaser-go-sofa.jpg","contentUrl":"https:\/\/www.pcjv.de\/wordpress\/wp-content\/uploads\/teaser-go-sofa.jpg","width":480,"height":360},{"@type":"BreadcrumbList","@id":"https:\/\/www.pcjv.de\/?p=2148#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.pcjv.de\/"},{"@type":"ListItem","position":2,"name":"go-sofa and go-hdf5: how a head hears, in pure Go"}]},{"@type":"WebSite","@id":"https:\/\/www.pcjv.de\/#website","url":"https:\/\/www.pcjv.de\/","name":"Christian&#039;s private site","description":"Private site of Christian-W. Budde","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.pcjv.de\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"}]}},"_links":{"self":[{"href":"https:\/\/www.pcjv.de\/index.php?rest_route=\/wp\/v2\/posts\/2148","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.pcjv.de\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.pcjv.de\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/www.pcjv.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2148"}],"version-history":[{"count":3,"href":"https:\/\/www.pcjv.de\/index.php?rest_route=\/wp\/v2\/posts\/2148\/revisions"}],"predecessor-version":[{"id":2152,"href":"https:\/\/www.pcjv.de\/index.php?rest_route=\/wp\/v2\/posts\/2148\/revisions\/2152"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.pcjv.de\/index.php?rest_route=\/wp\/v2\/media\/2169"}],"wp:attachment":[{"href":"https:\/\/www.pcjv.de\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2148"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.pcjv.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2148"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.pcjv.de\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2148"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}