<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Tales of Code]]></title><description><![CDATA[Thoughts, stories and ideas.]]></description><link>https://talesofcode.com/</link><image><url>https://talesofcode.com/favicon.png</url><title>Tales of Code</title><link>https://talesofcode.com/</link></image><generator>Ghost 4.12</generator><lastBuildDate>Wed, 18 Mar 2026 13:09:36 GMT</lastBuildDate><atom:link href="https://talesofcode.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Developing a Compass Android Application]]></title><description><![CDATA[A guide for developing a digital compass application on Android. Accelerometer and magnetometer sensor data will be fused to obtain a tilt compensated heading. Moreover, a low-pass filter is used to obtain smoother sensor data, and magnetic declination is calculated to obtain true heading.]]></description><link>https://talesofcode.com/developing-compass-android-application/</link><guid isPermaLink="false">5ecbc74b52a63308689edd6c</guid><category><![CDATA[Compass]]></category><category><![CDATA[Navigation]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Abdulrahman Al-Kibbe]]></dc:creator><pubDate>Sat, 22 Aug 2020 20:54:04 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1518065896235-a4c93e088e7a?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1518065896235-a4c93e088e7a?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Developing a Compass Android Application"><p>This post is accompanied by a sample application that can be found <a href="https://github.com/yogur/android-compass">on GitHub</a>. Feel free to clone the repository and try it out yourself.</p><h2 id="the-ui">The UI</h2><p>We will not discuss the UI elements in detail, but will focus instead on calculations and Java code. The code below is from the sample application&apos;s layout file. The layout contains three <code>TextView</code> elements for displaying magnetic heading, true heading and magnetic declination values. The <code>ImageView</code> contains an image resembling a compass. The image will be programmatically rotated with animation, based on magnetic heading value.</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;
    android:background=&quot;#e7cf8a&quot;
    &gt;

    &lt;LinearLayout
        android:id=&quot;@+id/linear_layout_values&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        app:layout_constraintBottom_toTopOf=&quot;@+id/image_compass&quot;
        app:layout_constraintLeft_toLeftOf=&quot;parent&quot;
        app:layout_constraintRight_toRightOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintVertical_chainStyle=&quot;spread&quot;&gt;

        &lt;TextView
            android:id=&quot;@+id/text_view_magnetic_declination&quot;
            android:layout_width=&quot;0dp&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:layout_weight=&quot;1&quot;
            android:gravity=&quot;center&quot;
            android:textStyle=&quot;bold&quot; /&gt;

        &lt;TextView
            android:id=&quot;@+id/text_view_heading&quot;
            android:layout_width=&quot;0dp&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:layout_weight=&quot;1&quot;
            android:gravity=&quot;center&quot;
            android:textStyle=&quot;bold&quot; /&gt;

        &lt;TextView
            android:id=&quot;@+id/text_view_true_heading&quot;
            android:layout_width=&quot;0dp&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:layout_weight=&quot;1&quot;
            android:gravity=&quot;center&quot;
            android:textStyle=&quot;bold&quot; /&gt;

    &lt;/LinearLayout&gt;

    &lt;ImageView
        android:id=&quot;@+id/image_compass&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:adjustViewBounds=&quot;true&quot;
        android:src=&quot;@drawable/compass&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintLeft_toLeftOf=&quot;parent&quot;
        app:layout_constraintRight_toRightOf=&quot;parent&quot;
        app:layout_constraintTop_toBottomOf=&quot;@+id/linear_layout_values&quot;
        app:layout_constraintWidth_percent=&quot;0.8&quot; /&gt;

&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;</code></pre><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://talesofcode.com/content/images/2020/08/Screenshot_2020-08-22-23-26-34-311_com.talesofcode.compass.jpg" class="kg-image" alt="Developing a Compass Android Application" loading="lazy"><figcaption>Resulting layout in sample application</figcaption></figure><h2 id="the-sensors-tilt-compensation">The Sensors &amp; Tilt Compensation</h2><p>A magnetometer is a sensor used for measuring magnetic fields. A magnetometer can measure Earth&apos;s magnetic field if not influenced by a strong nearby magnetic field. The sensor provides magnetic field strength along three axes X, Y and Z in micro Tesla (&#x3BC;T). </p><p>Earth&apos;s magnetic field is parallel to Earth&apos;s surface. If a device is parallel to Earth&apos;s surface, heading can be measured by using a magnetometer&apos;s X and Y components. However if the device is tilted, the heading value will no longer be accurate and tilt compensation should be performed by utilizing an accelerometer.</p><p>An accelerometer is a sensor that measures acceleration along the three axes X, Y and Z in meter per second squared (m/s<sup>2</sup>). Accelerometer readings will be used for magnetometer correction.</p><h2 id="obtaining-sensor-readings-on-android">Obtaining Sensor Readings on Android</h2><p>Sensor readings are obtained through the <code>SensorManager</code> API. Create a global <code>SensorManager</code> object and initialize it in the activity&apos;s <code>OnCreate</code> method.</p><pre><code class="language-java">private SensorManager sensorManager;

...

sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);</code></pre><p>In the <code>OnResume</code> method, register listeners on accelerometer and magnetic field sensors. We should also unregister the listeners in the <code>OnPause</code> method in case the application is closed.</p><pre><code class="language-java">    @Override
    protected void onResume() {
        super.onResume();

        Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        if (accelerometer != null) {
            sensorManager.registerListener(this, accelerometer,
                    SensorManager.SENSOR_DELAY_GAME, SensorManager.SENSOR_DELAY_GAME);
        }

        Sensor magneticField = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        if (magneticField != null) {
            sensorManager.registerListener(this, magneticField,
                    SensorManager.SENSOR_DELAY_GAME, SensorManager.SENSOR_DELAY_GAME);
        }
    }
    
    
    @Override
    protected void onPause() {
        super.onPause();
        sensorManager.unregisterListener(this);
    }</code></pre><p>Following that, implement the interface <code>SensorEventListener</code> and let Android Studio automatically implement the interface&apos;s method. The implemented method <code>onSensorChanged</code> will be called whenever new sensor values are received. Updated sensor values can be obtained from the <code>SensorEvent</code> argument.</p><p>In the code snippet below, we are receiving updated sensor values, passing them to our low-pass filter, then calling the <code>updateHeading()</code> function. The low-pass filter and heading calculation will be discussed in upcoming sections.</p><pre><code class="language-java">    private final float[] accelerometerReading = new float[3];
    private final float[] magnetometerReading = new float[3];

	...
    
        @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            //make sensor readings smoother using a low pass filter
            CompassHelper.lowPassFilter(event.values.clone(), accelerometerReading);
        } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
            //make sensor readings smoother using a low pass filter
            CompassHelper.lowPassFilter(event.values.clone(), magnetometerReading);
        }
        updateHeading();
    }</code></pre><h2 id="calculating-magnetic-heading">Calculating Magnetic Heading</h2><p>There are several methods for tilt compensation. We will use vector cross product method to obtain a tilt compensated heading angle (azimuth). </p><!--kg-card-begin: markdown--><p>Magnetic heading is obtained by performing the following:</p>
<ul>
<li>Calculating the cross product of the magnetic field vector and the gravity vector to get a new vector H</li>
<li>Normalizing the values of the resulting vector H and of the gravity vector</li>
<li>Calculating the cross product of the gravity vector and the vector H to get a new vector M pointing towards Earth&apos;s magnetic field</li>
<li>Using arctangent to obtain heading in radians</li>
<li>Converting the angle from radians to degrees</li>
<li>Mapping the heading angle from [-180,180] range to [0, 360] range</li>
</ul>
<!--kg-card-end: markdown--><p>The functions below implement these steps.</p><pre><code class="language-java">    public static float calculateHeading(float[] accelerometerReading, float[] magnetometerReading) {
        float Ax = accelerometerReading[0];
        float Ay = accelerometerReading[1];
        float Az = accelerometerReading[2];

        float Ex = magnetometerReading[0];
        float Ey = magnetometerReading[1];
        float Ez = magnetometerReading[2];

        //cross product of the magnetic field vector and the gravity vector
        float Hx = Ey * Az - Ez * Ay;
        float Hy = Ez * Ax - Ex * Az;
        float Hz = Ex * Ay - Ey * Ax;

        //normalize the values of resulting vector
        final float invH = 1.0f / (float) Math.sqrt(Hx * Hx + Hy * Hy + Hz * Hz);
        Hx *= invH;
        Hy *= invH;
        Hz *= invH;

        //normalize the values of gravity vector
        final float invA = 1.0f / (float) Math.sqrt(Ax * Ax + Ay * Ay + Az * Az);
        Ax *= invA;
        Ay *= invA;
        Az *= invA;

        //cross product of the gravity vector and the new vector H
        final float Mx = Ay * Hz - Az * Hy;
        final float My = Az * Hx - Ax * Hz;
        final float Mz = Ax * Hy - Ay * Hx;

        //arctangent to obtain heading in radians
        return (float) Math.atan2(Hy, My);
    }


    public static float convertRadtoDeg(float rad) {
        return (float) (rad / Math.PI) * 180;
    }

    //map angle from [-180,180] range to [0,360] range
    public static float map180to360(float angle) {
        return (angle + 360) % 360;
    }</code></pre><h2 id="implementing-a-low-pass-filter-for-smoother-sensor-data">Implementing a Low-Pass Filter for Smoother Sensor Data</h2><p>Using sensor data directly results in jittery heading values and compass rotation. Different filters can be used for obtaining smoother sensor data. However, filter algorithms can be very complex. We will implement a simple low-pass filter that provides us with nice and smooth results. Check the references section for further discussion on filters.</p><pre><code class="language-java">    //0 &#x2264; ALPHA &#x2264; 1
    //smaller ALPHA results in smoother sensor data but slower updates
    public static final float ALPHA = 0.15f;
    
    public static float[] lowPassFilter(float[] input, float[] output) {
        if (output == null) return input;

        for (int i = 0; i &lt; input.length; i++) {
            output[i] = output[i] + ALPHA * (input[i] - output[i]);
        }
        return output;
    }</code></pre><h2 id="magnetic-declination-and-true-heading">Magnetic Declination and True Heading</h2><p>Magnetized compass needles and magnetometers point towards Earth&apos;s magnetic field. Thus, magnetic declination value is required to obtain true heading. Magnetic declination is defined as the angle between magnetic north and true north. Declination is positive east of true north and negative when west.</p><p><code>true heading = magnetic heading + magnetic declination</code></p><p>Magnetic declination varies over time and with location. The change of magnetic field was observed over a period of years and two Geomagnetic models were created: International Geomagnetic Reference Field (IGRF) and World Magnetic Model (WMM). We will use the open source Java implementation of the WMM created by Los Alamos National Laboratory to calculate magnetic declination. WMM&apos;s coefficients have been updated in 2020 and are valid for five years. Longitude, latitude and altitude are required to calculate magnetic declination.</p><pre><code class="language-java">    public static float calculateMagneticDeclination(double latitude, double longitude, double altitude) {
        TSAGeoMag geoMag = new TSAGeoMag();
        return (float) geoMag
                .getDeclination(latitude, longitude, geoMag.decimalYear(new GregorianCalendar()), altitude);
    }</code></pre><h2 id="getting-last-known-location">Getting Last Known Location</h2><p>To obtain longitude, latitude and altitude, we will use the <code>FusedLocationProviderClient</code> to get the user&apos;s last known location. This client is provided by Google Play services. Check the developer guides in the references section for more details on setting up and using the <code>FusedLocationProviderClient</code>.</p><p>Make sure to include Google Play location services as a dependency in the <code>build.gradle</code> file, and to include the required location permissions in the <code>AndroidManifest.xml</code> file.</p><pre><code class="language-xml">&lt;uses-permission android:name=&quot;android.permission.ACCESS_FINE_LOCATION&quot; /&gt;</code></pre><p>Create and initialize a <code>FusedLocationProviderClient</code> object. However before requesting location data, we have to check if user has granted location permissions to the application. If permissions are not already granted, a prompt should be presented to the user requesting location permissions.</p><pre><code class="language-java">        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);

        //check if we have permission to access location
        if (ContextCompat.checkSelfPermission(
                this, Manifest.permission.ACCESS_FINE_LOCATION) ==
                PackageManager.PERMISSION_GRANTED) {
            //fine location permission already granted
            getLocation();
        } else {
            //if permission is not granted, request location permissions from user
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                    REQUEST_PERMISSION_FINE_LOCATION);
        }</code></pre><p>The method <code>onRequestPermissionsResult</code> should also be implemented to handle the location permissions result provided by the user. If the user rejects the permission prompt, an error message will be displayed.</p><pre><code class="language-java">    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                           int[] grantResults) {
        if (requestCode == REQUEST_PERMISSION_FINE_LOCATION) {
            //if request is cancelled, the result arrays are empty.
            if (grantResults.length &gt; 0 &amp;&amp;
                    grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //permission is granted
                getLocation();
            } else {
                //display Toast with error message
                Toast.makeText(this, R.string.location_error_msg, Toast.LENGTH_LONG).show();
            }
        }
    }</code></pre><p>If location permissions were granted, the function <code>getLocation</code> will be called to retrieve the required location data from the <code>FusedLocationProviderClient</code>, and to calculate magnetic declination.</p><pre><code class="language-java">    @SuppressLint(&quot;MissingPermission&quot;) //suppress warning since we have already checked for permissions before calling the function
    private void getLocation() {
        fusedLocationClient.getLastLocation()
                .addOnSuccessListener(this, new OnSuccessListener&lt;Location&gt;() {
                    @Override
                    public void onSuccess(Location location) {
                        // Got last known location. In some rare situations this can be null.
                        if (location != null) {
                            isLocationRetrieved = true;
                            latitude = (float) location.getLatitude();
                            longitude = (float) location.getLongitude();
                            altitude = (float) location.getAltitude();
                            magneticDeclination = CompassHelper.calculateMagneticDeclination(latitude, longitude, altitude);
                            textViewMagneticDeclination.setText(getString(R.string.magnetic_declination, magneticDeclination));
                        }
                    }
                });
    }</code></pre><h2 id="wrapping-it-all-up">Wrapping it All Up</h2><p>Now that we have written all supporting code, what remains is calculating heading and updating the UI accordingly. The function <code>updateHeading</code> is called by <code>onSensorChanged</code> on every sensor value change event. It calculates magnetic heading, then checks if location data is available and calculates true heading. Finally, <code>TextView</code> elements are updated, and the compass image is rotated with animation to simulate a real compass.</p><pre><code class="language-java">    private void updateHeading() {
        //oldHeading required for image rotate animation
        oldHeading = heading;

        heading = CompassHelper.calculateHeading(accelerometerReading, magnetometerReading);
        heading = CompassHelper.convertRadtoDeg(heading);
        heading = CompassHelper.map180to360(heading);

        if(isLocationRetrieved) {
            trueHeading = heading + magneticDeclination;
            if(trueHeading &gt; 360) { //if trueHeading was 362 degrees for example, it should be adjusted to be 2 degrees instead
                trueHeading = trueHeading - 360;
            }
            textViewTrueHeading.setText(getString(R.string.true_heading, (int) trueHeading));
        }

        textViewHeading.setText(getString(R.string.heading, (int) heading));

        RotateAnimation rotateAnimation = new RotateAnimation(-oldHeading, -heading, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        rotateAnimation.setDuration(500);
        rotateAnimation.setFillAfter(true);
        imageViewCompass.startAnimation(rotateAnimation);
    }</code></pre><p></p><!--kg-card-begin: markdown--><h1 id="references">References</h1>
<ul>
<li><a href="https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/hardware/SensorManager.java">SensorManager API Android open source implementation</a></li>
<li><a href="https://www.w3.org/TR/motion-sensors/#absolute-orientation">W3 Absolute Orientation Sensor</a></li>
<li><a href="https://www.mathworks.com/help/fusion/ref/ecompass.html#mw_ecf9e057-074a-4fe7-b63e-0d50d95946a0">Matlab E-Compass Algorithms</a></li>
<li><a href="http://blog.thomnichols.org/2011/08/smoothing-sensor-data-with-a-low-pass-filter">Thom Nichols - Smoothing sensor data with a low pass filter</a></li>
<li><a href="https://www.w3.org/TR/motion-sensors/#pass-filters">W3 Low and high pass filters</a></li>
<li><a href="https://www.ngdc.noaa.gov/geomag/declination.shtml">NOAA - Magnetic Declination</a></li>
<li><a href="https://www.ngdc.noaa.gov/geomag/WMM/thirdpartycontributions.shtml">NOAA - Geomagnetic Models Software Implementation</a></li>
<li><a href="https://developer.android.com/training/location">Android Location Developer Guide</a></li>
<li><a href="https://developer.android.com/guide/topics/permissions/overview">Android Permissions Developer Guide</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Setting Up an Icecast Audio Streaming Server]]></title><description><![CDATA[A guide for setting up an HTTPS audio stream over Icecast, and using Liquidsoap or BUTT as source clients.]]></description><link>https://talesofcode.com/icecast-audio-streaming-server/</link><guid isPermaLink="false">5ebd7f1e52a63308689ed966</guid><dc:creator><![CDATA[Abdulrahman Al-Kibbe]]></dc:creator><pubDate>Fri, 15 May 2020 17:22:15 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1519874179391-3ebc752241dd?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://images.unsplash.com/photo-1519874179391-3ebc752241dd?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Setting Up an Icecast Audio Streaming Server"><p>Icecast is an open source media streaming server that can stream both audio and video across a network. It is very versatile and customizable. It can be used for running live podcasts or for hosting internet radio stations.</p>
<p>In this guide, I will share my experience of setting up an HTTPS audio stream over Icecast. The guide will assume that you are running Ubuntu 18.04.</p>
<h1 id="thearchitecture">The Architecture</h1>
<p>Icecast requires a source client that sends a bitstream to a mountpoint. Listeners can listen to the stream by accessing these mountpoints.</p>
<p><img src="https://talesofcode.com/content/images/2020/05/800px-Flowchart_icecast.jpg" alt="Setting Up an Icecast Audio Streaming Server" loading="lazy"> <a href="https://wiki.xiph.org/File:Flowchart_icecast.png">Image from wiki.xiph.org</a></p>
<p>We will discuss setting up source clients later.</p>
<h1 id="installingicecast">Installing Icecast</h1>
<p>The Icecast package included in Ubuntu 18.04 repositories is not compiled with HTTPS support. Thus, we will add the <a href="https://wiki.xiph.org/Icecast_Server/Installing_latest_version_(official_Xiph_repositories)">official Xiph repository</a> which includes the latest Icecast package compiled with HTTPS support.</p>
<ol>
<li>Add the Xiph repository.</li>
</ol>
<pre><code>sudo sh -c &quot;echo deb http://download.opensuse.org/repositories/multimedia:/xiph/xUbuntu_18.04/ ./ &gt;&gt;/etc/apt/sources.list.d/icecast.list&quot;
</code></pre>
<ol start="2">
<li>Add the multimedia signing key as a trusted key.</li>
</ol>
<pre><code>wget -qO - http://icecast.org/multimedia-obs.key | sudo apt-key add -
</code></pre>
<ol start="3">
<li>Update apt package indexes.</li>
</ol>
<pre><code>sudo apt-get update
</code></pre>
<ol start="4">
<li>Install Icecast.</li>
</ol>
<pre><code>sudo apt-get install icecast2
</code></pre>
<ol start="5">
<li>Setup SSL.</li>
</ol>
<p>This section will assume that you already have a SSL certificate from <a href="https://letsencrypt.org/">Let&apos;s Encrypt</a> with certbot configured for certificate auto renewal. Certbot setup varies slightly depending on whether the server is already running a web server (Apache, Nginx, etc.) or not. The <a href="https://certbot.eff.org/instructions">instructions at EFF</a> are straightforward on how to setup certbot based on different scenarios.</p>
<p><strong>Note:</strong> You can choose to skip this step and run Icecast without enabling SSL.</p>
<p>Icecast requires an SSL certificate file that contains both the private and public key. Thus, we will concatenate the keys generated by Let&apos;s Encrypt, and then move them to our Icecast directory. After that, we will change the ownership of the new key file to Icecast user and group. This will allow the Icecast server to access the new certificate file.</p>
<pre><code>cat /etc/letsencrypt/live/talesofcode.com/fullchain.pem /etc/letsencrypt/live/talesofcode.com/privkey.pem &gt; /etc/icecast2/concat.pem

chown icecast2:icecast /etc/icecast2/concat.pem
</code></pre>
<p>Certificates issued by Let&apos;s Encrypt are valid for 90 days, and are automatically renewed by certbot. Thus, we should add a post hook to certbot that automatically combines the private and public keys for Icecast upon certificate renewal.</p>
<p>The following line should be added to the file <code>/etc/letsencrypt/renewal/talesofcode.com.conf</code> under the <code>[renewalparams]</code> section:</p>
<pre><code>post_hook = cat /etc/letsencrypt/live/talesofcode.com/fullchain.pem /etc/letsencrypt/live/talesofcode.com/privkey.pem &gt; /etc/icecast2/concat.pem &amp;&amp; service icecast2 restart
</code></pre>
<h1 id="configuringicecast">Configuring Icecast</h1>
<p>We will now configure Icecast by editing the Icecast configuration file.</p>
<pre><code>nano /etc/icecast2/icecast.xml
</code></pre>
<p>The parameters below are the main parameters required to get us up and running:</p>
<ul>
<li><code>&lt;source-password&gt;</code>: the plaintext password used by source clients to connect to Icecast.</li>
<li><code>&lt;admin-password&gt;</code>: the plaintext password used for web administration functions.</li>
<li><code>&lt;port&gt;</code> and <code>&lt;ssl&gt;</code> parameters under the <code>&lt;listen-socket&gt;</code> parameter.</li>
<li>mountpoint parameters under <code>mount</code>.</li>
<li><code>&lt;ssl-certificate&gt;</code> parameter under <code>paths</code>.</li>
</ul>
<p>Refer to <a href="https://www.icecast.org/docs/icecast-2.4.1/config-file.html">Icecast documentation</a> for details on all supported parameters.</p>
<p>The snippet below contains the full <code>icecast.xml</code> file with all parameters set.</p>
<!--kg-card-end: markdown--><pre><code class="language-xml">&lt;icecast&gt;
    &lt;location&gt;Earth&lt;/location&gt;
    &lt;admin&gt;icemaster@localhost&lt;/admin&gt;

    &lt;limits&gt;
        &lt;clients&gt;12000&lt;/clients&gt;
        &lt;sources&gt;2&lt;/sources&gt;
        &lt;queue-size&gt;524288&lt;/queue-size&gt;
        &lt;client-timeout&gt;30&lt;/client-timeout&gt;
        &lt;header-timeout&gt;15&lt;/header-timeout&gt;
        &lt;source-timeout&gt;10&lt;/source-timeout&gt;
        &lt;burst-on-connect&gt;1&lt;/burst-on-connect&gt;
        &lt;burst-size&gt;65535&lt;/burst-size&gt;
    &lt;/limits&gt;

    &lt;authentication&gt;
        &lt;!-- Sources log in with username &apos;source&apos; --&gt;
        &lt;source-password&gt;secret-pass-here&lt;/source-password&gt;
        &lt;!-- Relays log in with username &apos;relay&apos; --&gt;
        &lt;relay-password&gt;disabled&lt;/relay-password&gt;

        &lt;!-- Admin logs in with the username given below --&gt;
        &lt;admin-user&gt;ice-admin&lt;/admin-user&gt;
        &lt;admin-password&gt;secret-pass-here&lt;/admin-password&gt;
    &lt;/authentication&gt;

    &lt;hostname&gt;talesofcode.com&lt;/hostname&gt;

    &lt;!-- You may have multiple &lt;listener&gt; elements --&gt;
    &lt;listen-socket&gt;
        &lt;port&gt;8443&lt;/port&gt;
        &lt;ssl&gt;1&lt;/ssl&gt;
    &lt;/listen-socket&gt;
    
	
    &lt;http-headers&gt;
        &lt;header name=&quot;Access-Control-Allow-Origin&quot; value=&quot;*&quot; /&gt;
    &lt;/http-headers&gt;


    &lt;!-- Mountpoints--&gt;
    &lt;mount&gt;
        &lt;mount-name&gt;/stream.mp3&lt;/mount-name&gt;
        &lt;stream-name&gt;Tales of Code Live Stream&lt;/stream-name&gt;
        &lt;stream-description&gt;Tales of Code Live Stream&lt;/stream-description&gt;
        &lt;bitrate&gt;192&lt;/bitrate&gt;
        &lt;type&gt;audio/mpeg&lt;/type&gt;
    &lt;/mount&gt;

    &lt;fileserve&gt;1&lt;/fileserve&gt;

    &lt;paths&gt;
        &lt;!-- basedir is only used if chroot is enabled --&gt;
        &lt;basedir&gt;/usr/share/icecast2&lt;/basedir&gt;

        &lt;!-- Note that if &lt;chroot&gt; is turned on below, these paths must both
             be relative to the new root, not the original root --&gt;
        &lt;logdir&gt;/usr/share/icecast2/logs&lt;/logdir&gt;
        &lt;webroot&gt;/usr/share/icecast2/web&lt;/webroot&gt;
        &lt;adminroot&gt;/usr/share/icecast2/admin&lt;/adminroot&gt;
        &lt;!-- &lt;pidfile&gt;/usr/share/icecast2/icecast.pid&lt;/pidfile&gt; --&gt;

        &lt;alias source=&quot;/&quot; destination=&quot;/status.xsl&quot;/&gt;
        &lt;!-- The certificate file needs to contain both public and private part.
             Both should be PEM encoded. --&gt;
        &lt;ssl-certificate&gt;/etc/icecast2/concat.pem&lt;/ssl-certificate&gt;
    &lt;/paths&gt;

    &lt;logging&gt;
        &lt;accesslog&gt;access.log&lt;/accesslog&gt;
        &lt;errorlog&gt;error.log&lt;/errorlog&gt;
        &lt;loglevel&gt;3&lt;/loglevel&gt; &lt;!-- 4 Debug, 3 Info, 2 Warn, 1 Error --&gt;
        &lt;logsize&gt;10000&lt;/logsize&gt;
    &lt;/logging&gt;

    &lt;security&gt;
        &lt;chroot&gt;0&lt;/chroot&gt; &lt;!-- chroot disabled --&gt;
        &lt;changeowner&gt;
            &lt;user&gt;icecast2&lt;/user&gt;
            &lt;group&gt;icecast&lt;/group&gt;
        &lt;/changeowner&gt; 
    &lt;/security&gt;
&lt;/icecast&gt;
</code></pre><!--kg-card-begin: markdown--><p>Save and close the xml file. You should now be able to start the Icecast server using the command below:</p>
<pre><code>icecast -b -c /etc/icecast2/icecast.xml
</code></pre>
<p>If all is well, Icecast status page should now be accessible from a web browser on port 8443 <code>https://your-domain-or-ip:8443</code>. Make sure that the port 8443 is allowed on the <code>ufw</code> firewall.</p>
<p>The last step of server side configuration is to enable Icecast on system reboot.</p>
<pre><code>systemctl enable icecast2
</code></pre>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="settingupthesourceclient">Setting Up the Source Client</h1>
<p>This section will demonstrate how to use <a href="https://danielnoethen.de/butt/">Broadcast Using this Tool (BUTT)</a> or <a href="https://www.liquidsoap.info/">Liquidsoap</a> as source clients. These source clients can be used to stream directly from Line input, or to stream from an audio file. BUTT offers a graphical interface, while Liquidsoap has a scripting language and is launched from the command line. You can use either of them as your source client.</p>
<p><strong>Option 1: Broadcast Using this Tool (BUTT)</strong></p>
<ol>
<li>Download and install BUTT (varies based on OS of choice).</li>
<li>Launch BUTT and go to &quot;Settings&quot;.</li>
<li>Add a server with your Icecast server details.</li>
</ol>
<p><img src="https://talesofcode.com/content/images/2020/05/butt1.png" alt="Setting Up an Icecast Audio Streaming Server" loading="lazy"></p>
<p><img src="https://talesofcode.com/content/images/2020/05/butt2.png" alt="Setting Up an Icecast Audio Streaming Server" loading="lazy"></p>
<ol start="4">
<li>Navigate to the &quot;Audio&quot; tab to configure your input device and streaming codec.</li>
</ol>
<p><img src="https://talesofcode.com/content/images/2020/05/butt3.png" alt="Setting Up an Icecast Audio Streaming Server" loading="lazy"></p>
<ol start="5">
<li>Go back to the &quot;Main&quot; tab and hit the &quot;Save&quot; button.</li>
<li>Click the play button in BUTT&apos;s main window. You should see that BUTT has successfully connected to your Icecast server.</li>
<li>Visit <code>https://your-ip-or-domain:8443/stream.mp3</code>. You should now be able to listen to the audio stream sent by BUTT on the Icecast mountpoint.</li>
</ol>
<p><strong>Option 2: Liquidsoap</strong></p>
<ol>
<li>Download and setup Liquidsoap (varies based on OS of choice).</li>
<li>Create a <code>liq</code> file which will contain our Liquidsoap configuration.</li>
<li>Copy the code snippet below to your <code>liq</code> file. Set <code>host</code> to your domain name or IP address, and <code>password</code> to your source password configured in Icecast server.</li>
</ol>
<pre><code>set(&quot;log.stdout&quot;,true)
set(&quot;log.file&quot;,false)

# Input from default audio port
s = input.portaudio()

# An icecast output in mp3 format
output.icecast(%mp3(bitrate=192),
              host=&quot;talesofcode.com&quot;, port = 8443,
			  password = &quot;source-password-here&quot;,
              fallible=true,
              mount=&quot;stream.mp3&quot;,
              s)
</code></pre>
<ol start="4">
<li>Start Liquidsoap from the command line, passing the <code>liq</code> file as input.</li>
</ol>
<pre><code>liquidsoap file.liq
</code></pre>
<ol start="5">
<li>You should see that Liquidsoap has successfully connected to your Icecast server.</li>
<li>Visit <code>https://your-ip-or-domain:8443/stream.mp3</code>. You should now be able to listen to the audio stream from your source client on the Icecast mountpoint.</li>
</ol>
<p>Liquidsoap is a very powerful open source tool. Refer to its <a href="https://www.liquidsoap.info/doc-1.4.2/">documentation</a> for advanced examples and use cases.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>