From 24d8258430114c20aae7715bd29b2af7302348bd Mon Sep 17 00:00:00 2001
From: Moritz Bunkus <moritz@bunkus.org>
Date: Fri, 4 Nov 2011 22:40:36 +0100
Subject: [PATCH] XSLT 2.0 stylesheets for chapters to: cue sheets; shnsplit
 timecodes

---
 ChangeLog                                     |   6 +
 examples/stylesheets/chapters-to-cuesheet.xsl | 152 ++++++++++++++++++
 examples/stylesheets/chapters-to-shnsplit.xsl | 137 ++++++++++++++++
 3 files changed, 295 insertions(+)
 create mode 100644 examples/stylesheets/chapters-to-cuesheet.xsl
 create mode 100644 examples/stylesheets/chapters-to-shnsplit.xsl

diff --git a/ChangeLog b/ChangeLog
index 172ac8175..e48ae19b4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,11 @@
 2011-11-04  Moritz Bunkus  <moritz@bunkus.org>
 
+	* examples: Added XSLT 2.0 stylesheets in the
+	"examples/stylesheets" directory for turning Matroska chapters
+	into cue sheet and split points for "shntool" (useful for
+	situations in which you have e.g. a live recording from a concert
+	including chapters and want to create one audio file per song).
+
 	* mkvmerge: bug fix: Fixed reading VC1 video tracks from Matroska
 	files that don't use VC1 start markers (0x00 0x00 0x01 ...).
 
diff --git a/examples/stylesheets/chapters-to-cuesheet.xsl b/examples/stylesheets/chapters-to-cuesheet.xsl
new file mode 100644
index 000000000..a0ec88f9f
--- /dev/null
+++ b/examples/stylesheets/chapters-to-cuesheet.xsl
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+
+Chapters to cue sheet
+=====================
+
+This is a XSLT 2.0 stylesheet for turning the chapter format that mkvmerge
+understands into a cue sheet as used by e.g. CD recording software.
+
+You need a XSLT 2.0 compatible XSLT processor, e.g. Saxon-HE (see
+http://saxon.sourceforge.net/).
+
+Usage depends on your processor. For Saxon-HE on Linux this should work:
+
+$ java -classpath saxon9he.jar net.sf.saxon.Transform \
+    -o:output-filename.cue \
+    -xsl:chapters-to-cuesheet.xsl \
+    input-filename.xml
+
+You have to extract the chapters from a Matroska file with mkvextract first if
+you want to turn the chapters included in one into a cue sheet.
+
+This can be useful if you want to turn an audio file contain in a Matroska
+file into one file per chapter (e.g. for music videos). For this you need both
+"chapters-to-cuesheet.xsl" and "chapters-to-shnsplit.xsl" as well as the
+"cuetools" (see https://github.com/svend/cuetools) and "shntool" (see
+http://etree.org/shnutils/shntool/) packages.
+
+Assumptions are (meaning: adjust these values in the commands):
+
+- the source file contains an audio track with track ID 2 of type FLAC,
+- the band is "Perpetuum Jazzile",
+- the album is "Live 2009",
+- the source file name is "source.mkv".
+
+Here we go:
+
+  # 1. Extract audio track from Matroska file:
+  $ mkvextract source.mkv tracks 2:source.flac
+
+  # 2. Extract chapters from same file:
+  $ mkvextract source.mkv chapters > chapters.xml
+
+  # 3. Generate the split points used in step 4:
+  $ java -classpath saxon9he.jar net.sf.saxon.Transform \
+      -o:splitpoints.txt \
+      -xsl:chapters-to-cuesheet.xsl \
+      chapters.xml
+
+  # 4. Split the audio file into multiple files:
+  $ shnsplit -o flac source.flac < splitpoints.txt
+
+  # 5. Generate the cue sheet used in step 6:
+  $ java -classpath saxon9he.jar net.sf.saxon.Transform \
+      -o:source.cue \
+      -xsl:chapters-to-cuesheet.xsl \
+      chapters.xml \
+      artist="Perpetuum Jazzile" \
+      album="Live 2009"
+
+  # 6: Assign tags (artist and title):
+  $ cuetag source.cue split-*.flac
+
+Written by Moritz Bunkus <moritz@bunkus.org>.
+
+-->
+
+<xsl:stylesheet version="2.0"
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xsl:param name="album" select="'ChangeMeAlbum'"/>
+ <xsl:param name="artist" select="'ChangeMeAlbumArtist'"/>
+ <xsl:param name="track-artist" select="if ($artist eq 'ChangeMeAlbumArtist') then 'ChangeMeTrackArtist' else $artist"/>
+
+ <xsl:output method="text"/>
+ <xsl:strip-space elements="*"/>
+
+ <xsl:template match="/Chapters">
+  <xsl:apply-templates select="EditionEntry"/>
+ </xsl:template>
+
+ <xsl:template name="timecode-to-ns" as="xs:integer">
+  <xsl:param name="timecode" select="'0'"/>
+  <xsl:choose>
+   <xsl:when test="$timecode eq ''">
+    <xsl:value-of select="0"/>
+   </xsl:when>
+   <xsl:when test="matches($timecode, '^\d+:\d+:\d+\.\d{9}$')">
+    <xsl:analyze-string select="$timecode"
+                        regex="^ (\d+) : (\d+) : (\d+) \. (\d+) $"
+                        flags="x">
+     <xsl:matching-substring>
+      <xsl:value-of select="  (regex-group(1) cast as xs:integer) * 3600000000000
+                            + (regex-group(2) cast as xs:integer) *   60000000000
+                            + (regex-group(3) cast as xs:integer) *    1000000000
+                            + (regex-group(4) cast as xs:integer)"/>
+     </xsl:matching-substring>
+    </xsl:analyze-string>
+   </xsl:when>
+   <xsl:when test="matches($timecode, '^\d+:\d+:\d+\.\d*$')">
+    <xsl:call-template name="timecode-to-ns">
+     <xsl:with-param name="timecode" select="concat($timecode, '0')"/>
+    </xsl:call-template>
+   </xsl:when>
+   <xsl:when test="matches($timecode, '^\d+:\d+:\d+$')">
+    <xsl:call-template name="timecode-to-ns">
+     <xsl:with-param name="timecode" select="concat($timecode, '.')"/>
+    </xsl:call-template>
+   </xsl:when>
+   <xsl:otherwise>
+    <xsl:call-template name="timecode-to-ns">
+     <xsl:with-param name="timecode" select="concat('0:', $timecode)"/>
+    </xsl:call-template>
+   </xsl:otherwise>
+  </xsl:choose>
+ </xsl:template>
+
+ <xsl:template match="EditionEntry">
+  <xsl:text>PERFORMER "</xsl:text>
+  <xsl:value-of select="$artist"/>
+  <xsl:text>"
+TITLE "</xsl:text>
+  <xsl:value-of select="$album"/>
+  <xsl:text>"
+FILE "ChangeMe.mp3" MP3
+</xsl:text>
+  <xsl:for-each select="ChapterAtom">
+   <xsl:variable name="timecode" as="xs:integer">
+    <xsl:call-template name="timecode-to-ns">
+     <xsl:with-param name="timecode" select="ChapterTimeStart"/>
+    </xsl:call-template>
+   </xsl:variable>
+   <xsl:text>  TRACK </xsl:text>
+   <xsl:number format="01" value="position()"/>
+   <xsl:text> AUDIO
+    PERFORMER "</xsl:text>
+   <xsl:value-of select="$track-artist"/>
+   <xsl:text>"
+    TITLE "</xsl:text>
+   <xsl:value-of select="ChapterDisplay[1]/ChapterString"/>
+   <xsl:text>"
+    INDEX 01 </xsl:text>
+    <xsl:number format="01" value=" $timecode idiv 60000000000"/>
+    <xsl:text>:</xsl:text>
+    <xsl:number format="01" value="($timecode idiv  1000000000) mod 60"/>
+    <xsl:text>:</xsl:text>
+   <xsl:value-of select="($timecode mod 1000000000) * 75 idiv 1000000000"/>
+   <xsl:text>&#xa;</xsl:text>
+  </xsl:for-each>
+ </xsl:template>
+</xsl:stylesheet>
diff --git a/examples/stylesheets/chapters-to-shnsplit.xsl b/examples/stylesheets/chapters-to-shnsplit.xsl
new file mode 100644
index 000000000..c348818e4
--- /dev/null
+++ b/examples/stylesheets/chapters-to-shnsplit.xsl
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+
+Chapters to shnsplit
+====================
+
+This is a XSLT 2.0 stylesheet for turning the chapter format that mkvmerge
+understands into a list of timecodes for the "shnsplit" tool from the
+"shntool" package (see http://etree.org/shnutils/shntool/). It allows you to
+split a large audio file into smaller ones, optionally encoding/decoding in
+the process.
+
+You need a XSLT 2.0 compatible XSLT processor, e.g. Saxon-HE (see
+http://saxon.sourceforge.net/).
+
+Usage depends on your processor. For Saxon-HE on Linux this should work:
+
+  $ java -classpath saxon9he.jar net.sf.saxon.Transform \
+      -o:splitpoints.txt \
+      -xsl:chapters-to-shnsplit.xsl \
+      input-filename.xml \
+      artist="Name of the artist" \
+      album="Name of the album"
+
+You have to extract the chapters from a Matroska file with mkvextract first if
+you want to turn the chapters included in one into split points.
+
+This can be useful if you want to turn an audio file contain in a Matroska
+file into one file per chapter (e.g. for music videos). For this you need both
+"chapters-to-cuesheet.xsl" and "chapters-to-shnsplit.xsl" as well as the
+"cuetools" (see https://github.com/svend/cuetools) and "shntool" (see
+http://etree.org/shnutils/shntool/) packages.
+
+Assumptions are (meaning: adjust these values in the commands):
+
+- the source file contains an audio track with track ID 2 of type FLAC,
+- the band is "Perpetuum Jazzile",
+- the album is "Live 2009",
+- the source file name is "source.mkv".
+
+Here we go:
+
+  # 1. Extract audio track from Matroska file:
+  $ mkvextract source.mkv tracks 2:source.flac
+
+  # 2. Extract chapters from same file:
+  $ mkvextract source.mkv chapters > chapters.xml
+
+  # 3. Generate the split points used in step 4:
+  $ java -classpath saxon9he.jar net.sf.saxon.Transform \
+      -o:splitpoints.txt \
+      -xsl:chapters-to-cuesheet.xsl \
+      chapters.xml
+
+  # 4. Split the audio file into multiple files:
+  $ shnsplit -o flac source.flac < splitpoints.txt
+
+  # 5. Generate the cue sheet used in step 6:
+  $ java -classpath saxon9he.jar net.sf.saxon.Transform \
+      -o:source.cue \
+      -xsl:chapters-to-cuesheet.xsl \
+      chapters.xml \
+      artist="Perpetuum Jazzile" \
+      album="Live 2009"
+
+  # 6: Assign tags (artist and title):
+  $ cuetag source.cue split-*.flac
+
+Written by Moritz Bunkus <moritz@bunkus.org>.
+
+-->
+
+<xsl:stylesheet version="2.0"
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xsl:output method="text"/>
+ <xsl:strip-space elements="*"/>
+
+ <xsl:template match="/Chapters">
+  <xsl:apply-templates select="EditionEntry"/>
+ </xsl:template>
+
+ <xsl:template name="timecode-to-ns" as="xs:integer">
+  <xsl:param name="timecode" select="'0'"/>
+  <xsl:choose>
+   <xsl:when test="$timecode eq ''">
+    <xsl:value-of select="0"/>
+   </xsl:when>
+   <xsl:when test="matches($timecode, '^\d+:\d+:\d+\.\d{9}$')">
+    <xsl:analyze-string select="$timecode"
+                        regex="^ (\d+) : (\d+) : (\d+) \. (\d+) $"
+                        flags="x">
+     <xsl:matching-substring>
+      <xsl:value-of select="  (regex-group(1) cast as xs:integer) * 3600000000000
+                            + (regex-group(2) cast as xs:integer) *   60000000000
+                            + (regex-group(3) cast as xs:integer) *    1000000000
+                            + (regex-group(4) cast as xs:integer)"/>
+     </xsl:matching-substring>
+    </xsl:analyze-string>
+   </xsl:when>
+   <xsl:when test="matches($timecode, '^\d+:\d+:\d+\.\d*$')">
+    <xsl:call-template name="timecode-to-ns">
+     <xsl:with-param name="timecode" select="concat($timecode, '0')"/>
+    </xsl:call-template>
+   </xsl:when>
+   <xsl:when test="matches($timecode, '^\d+:\d+:\d+$')">
+    <xsl:call-template name="timecode-to-ns">
+     <xsl:with-param name="timecode" select="concat($timecode, '.')"/>
+    </xsl:call-template>
+   </xsl:when>
+   <xsl:otherwise>
+    <xsl:call-template name="timecode-to-ns">
+     <xsl:with-param name="timecode" select="concat('0:', $timecode)"/>
+    </xsl:call-template>
+   </xsl:otherwise>
+  </xsl:choose>
+ </xsl:template>
+
+ <xsl:template match="EditionEntry">
+  <xsl:for-each select="ChapterAtom">
+   <xsl:variable name="timecode" as="xs:integer">
+    <xsl:call-template name="timecode-to-ns">
+     <xsl:with-param name="timecode" select="ChapterTimeStart"/>
+    </xsl:call-template>
+   </xsl:variable>
+   <xsl:if test="$timecode &gt; 0">
+    <xsl:number format="01" value=" $timecode idiv 60000000000"/>
+    <xsl:text>:</xsl:text>
+    <xsl:number format="01" value="($timecode idiv  1000000000) mod 60"/>
+    <xsl:text>.</xsl:text>
+    <xsl:number format="001" value="($timecode mod 1000000000) idiv 1000000"/>
+    <xsl:text>&#xa;</xsl:text>
+   </xsl:if>
+  </xsl:for-each>
+ </xsl:template>
+</xsl:stylesheet>