threeeye
Posts: 4
Joined: Thu Nov 30, 2023 3:47 pm

DIY RPi 4 VoIP phone?

Thu Nov 30, 2023 3:55 pm

Hi guys,
I have a RPi4 that is not currently in use, so I wanted to try a project.
I'm tired of my VoIP phone in my office, and I'd like to DIY a VoIP phone.
I saw this phone (Sangoma P370 - https://www.sangoma.com/products/phones ... ive-phones) and I really like it, but it's expensive, I know this is an Android base phone, but I was wondering if I can make something similar to it (Linux base - my RPi is only 1gb RAM).
Any suggestions?

Thanks

EDIT:
For the mods: Sorry for posting this in "Media Centers", I meant to post this in "Other projects", if you can move it, please do.

drgeoff
Posts: 13943
Joined: Wed Jan 25, 2012 6:39 pm

Re: DIY RPi 4 VoIP phone?

Thu Nov 30, 2023 5:58 pm

Use the "Report this post" icon (looks like a flag) to get a moderator's attention. There is a field where you can write what you would like done.
Quis custodiet ipsos custodes?

knute
Posts: 942
Joined: Thu Oct 23, 2014 12:14 am
Location: Texas

Re: DIY RPi 4 VoIP phone?

Sat Dec 02, 2023 7:20 pm

threeeye wrote:
Thu Nov 30, 2023 3:55 pm
Hi guys,
I'm tired of my VoIP phone in my office, and I'd like to DIY a VoIP phone.

Any suggestions?
I wrote a Java intercom program. You can have the source if you wish.

User avatar
RosettaStone
Posts: 17
Joined: Mon Oct 09, 2023 7:23 am

Re: DIY RPi 4 VoIP phone?

Mon Dec 04, 2023 6:34 am

You could start by trying Linphone (it's in the repositories) and then, if you wanted to, writing your own client using the appropriate libraries (the Linphone and other SIP libraries are also in the repositories).

threeeye
Posts: 4
Joined: Thu Nov 30, 2023 3:47 pm

Re: DIY RPi 4 VoIP phone?

Mon Dec 04, 2023 3:16 pm

knute wrote:
Sat Dec 02, 2023 7:20 pm
I wrote a Java intercom program. You can have the source if you wish.
if it's a softphone, then sure
Thanks
RosettaStone wrote:
Mon Dec 04, 2023 6:34 am
You could start by trying Linphone (it's in the repositories) and then, if you wanted to, writing your own client using the appropriate libraries (the Linphone and other SIP libraries are also in the repositories).
I installed Linphone, but I didn't manage to get it to register the ext...
I tried different settings and extensions

threeeye
Posts: 4
Joined: Thu Nov 30, 2023 3:47 pm

Re: DIY RPi 4 VoIP phone?

Mon Dec 04, 2023 8:02 pm

Update:
I managed to get the extension to register.
But I don't like Linphone, it doesn't show a dial pad (maybe I'm missing a setting?).
Aside from Linphone, is there other programs?

knute
Posts: 942
Joined: Thu Oct 23, 2014 12:14 am
Location: Texas

Re: DIY RPi 4 VoIP phone?

Tue Dec 05, 2023 7:45 pm

threeeye wrote:
Mon Dec 04, 2023 3:16 pm
knute wrote:
Sat Dec 02, 2023 7:20 pm
I wrote a Java intercom program. You can have the source if you wish.
if it's a softphone, then sure
Thanks
RosettaStone wrote:
Mon Dec 04, 2023 6:34 am
You could start by trying Linphone (it's in the repositories) and then, if you wanted to, writing your own client using the appropriate libraries (the Linphone and other SIP libraries are also in the repositories).
I installed Linphone, but I didn't manage to get it to register the ext...
I tried different settings and extensions
I don't know what a softphone is but here you go:

Code: Select all

package com.knutejohnson.pi.intercom;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.sound.sampled.*;
import javax.swing.*;

/**
 * <p>
 * Intercom is a simple Java program to send voice over IP to a remote copy
 * of the Intercom program.
 * </p>
 *
 * <table class="striped">
 * <caption>Program Revisions</caption>
 * <tr><td>0.10<td style="white-space: nowrap;">01 Sep 2017<td>Incept
 * <tr><td>0.11<td>01 Oct 2017<td>Complete the javadocs
 * <tr><td>0.12<td>30 Oct 2017<td>Move the DatagramPacket creation out of the inner loops in the sendThread and the receiveThread
 * <tr><td>0.13<td>31 Oct 2017<td>Add code to close AudioInputStream in audioThread and the DatagramSocket in the receiveThread if an Exception occurs in either thread
 * <tr><td>0.14<td>23 Mar 2019<td>Fix bug in sendThread so it will use the new address after a change
 * <tr><td>0.15<td>25 Mar 2019<td>Clean up previous bug fix in sendThread
 * <tr><td>0.16<td>26 Mar 2019<td>Create the TargetDataLine for the microphone in the constructor instead of creating a new one each time the talk button pressed
 * <tr><td>0.17<td>15 Jun 2019<td>Clean up buffers for microphone TargetDataLine
 * <tr><td>0.18<td>28 Apr 2020<td>Housekeeping
 * <tr><td>0.19<td>14 Nov 2020<td>Clean up documentation
 * </table>
 *
 * @author Knute Johnson
 * @version 0.19 - 14 November 2020
 */
public class Intercom extends JFrame implements Runnable {
    /** Serial version UID */
    private static final long serialVersionUID = 1L;

    /** Program Version */
    public static final String VERSION = "0.18";

    /** Program Date */
    public static final String DATE = "28 April 2020";

    /** Default port number */
    private static final String DEFAULT_PORT = "56789";

    /** Default destination address */
    private static final String DEFAULT_ADDRESS = "localhost";

    /** User home directory */
    private static final File USER_HOME =
     new File(System.getProperty("user.home"));

    /** Properties file */
    private static final File PROPERTIES_FILE =
     new File(USER_HOME,".intercom");

    /** PCM_SIGNED AudioFormat */
    private static final AudioFormat PCM =
     new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,8000f,16,1,2,8000f,false);

    /** ULAW AudioFormat */
    private static final AudioFormat ULAW =
     new AudioFormat(AudioFormat.Encoding.ULAW,8000f,8,1,1,8000f,false);

    /** ALAW AudioFormat */
    private static final AudioFormat ALAW =
     new AudioFormat(AudioFormat.Encoding.ALAW,8000f,8,1,1,8000f,false);

    /** Program properties */
    private final Properties properties = new Properties();

    /** Transmit RingBuffer to access audio data from TargetDataLine in a
     *  AudioInputStream so that the format can be changed to ULAW */
    private transient final RingBuffer txBuf = new RingBuffer(10000);

    /** Receive RingBuffer to access received audio data in a AudioInptStream
     *  so that the format can be change back to PCM_SIGNED */
    private transient final RingBuffer rxBuf = new RingBuffer(10000);

    /** Thread that reads data from the transmit RingBuffer, assembles it into
     *  a DatagramPacket and transmits it */
    private transient final Thread sendThread;
    /** Thread that reads DatagramPackets and writes the data to the receive
     *  RingBuffer */
    private transient final Thread receiveThread;

    /** Thread that reads the receive RingBuffer, converts the data to
     *  PCM_SIGNED format and writes it to the SourceDataLine */
    private transient final Thread audioThread;

    /** A JMenu that displays a moving symbol, changed when datagram packets
     *  are received */
    private final ActivityJMenu activity;

    /** Flag to control the thread reading data from the TargetDataLine */
    private volatile boolean runFlag;

    /** TargetDataLine to collect audio from the microphone */
    private transient volatile TargetDataLine tdl;

    /** DatagramSocket used to receive packets of audio data from another copy
     *  of Intercom */
    private transient volatile DatagramSocket rxSocket;

    /**
     * Creates a new Intercom GUI and three of the data transfer threads
     */
    public Intercom() {
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        loadProperties(properties);

        JMenuBar menuBar = new JMenuBar();
        setJMenuBar(menuBar);

        JMenu file = menuBar.add(new JMenu("File"));
        JMenu help = menuBar.add(new JMenu("Help"));
        activity = new ActivityJMenu();
        menuBar.add(activity);

        JMenuItem mi;

        mi = file.add("Set Port");
        mi.addActionListener(event -> {
            String value = JOptionPane.showInputDialog(this,"Enter Port",
             properties.getProperty("port",DEFAULT_PORT));
            if (value != null) {
                try {
                    int port = Integer.parseInt(value);
                    if (port < 1024 || port > 65535)
                        throw new IllegalArgumentException(
                         "port number out of range (1024 - 65535)");
                    properties.put("port",value);
                    storeProperties(properties);
                    rxSocket.close();
                } catch (IllegalArgumentException iae) {
                    JOptionPane.showMessageDialog(this,iae,"Invalid Port",
                     JOptionPane.ERROR_MESSAGE);
                }
            }
        });
        mi = file.add("Set Address");
        mi.addActionListener(event -> {
            String value = JOptionPane.showInputDialog(this,"Enter Address",
             properties.getProperty("address",DEFAULT_ADDRESS));
            if (value != null) {
                try {
                    InetAddress inet = InetAddress.getByName(value);
                    properties.setProperty("address",value);
                    storeProperties(properties);
                } catch (UnknownHostException uhe) {
                    JOptionPane.showMessageDialog(this,uhe,"Invalid Address",
                     JOptionPane.ERROR_MESSAGE);
                }
            }
        });
        file.add(new JSeparator());
        mi = file.add("Quit");
        mi.addActionListener(event -> dispose());

        mi = help.add("About"); 
        mi.addActionListener(event -> JOptionPane.showMessageDialog(this,
         "Intercom\n" +
         "Version: " + VERSION + " - " + DATE +
         "\nWritten by: Knute Johnson","About Intercom",
         JOptionPane.INFORMATION_MESSAGE));

        JLabel talkButton = new JLabel("TALK",JLabel.CENTER);
        talkButton.setOpaque(true);
        talkButton.setBackground(Color.GREEN.darker());
        talkButton.setForeground(Color.WHITE);
        talkButton.setBorder(BorderFactory.createEmptyBorder(30,30,30,30));
        talkButton.setFont(new Font(Font.SANS_SERIF,Font.BOLD,48));
        talkButton.addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent me) {
                talkButton.setBackground(Color.RED.darker());
                runFlag = true;
                new Thread(Intercom.this).start();
            }
            public void mouseReleased(MouseEvent me) {
                talkButton.setBackground(Color.GREEN.darker());
                runFlag = false;
            }
        });
        add(talkButton,BorderLayout.CENTER);

        pack();
        setLocationRelativeTo(null);
        setVisible(true);

        try {
            tdl = AudioSystem.getTargetDataLine(PCM);
            //tdl.addLineListener(event ->
            // System.out.printf("TDL: %s%n",event.getType()));
            tdl.open();
        } catch (LineUnavailableException lue) {
            lue.printStackTrace();
            JOptionPane.showMessageDialog(this,lue,"FATAL ERROR",
             JOptionPane.ERROR_MESSAGE);
            dispose();
        }

        // The sendThread creates the sending DatagramSocket, reads data from
        // the transmit RingBuffer, converts it to ULAW and transmits it in a
        // datagram to the other Intercom program.
        sendThread = new Thread(() -> {
            while (true) {
                try (DatagramSocket socket = new DatagramSocket();
                 AudioInputStream pcm = new AudioInputStream(
                  txBuf.getInputStream(),PCM,AudioSystem.NOT_SPECIFIED);
                 AudioInputStream ais =
                  AudioSystem.getAudioInputStream(ULAW,pcm)) {

                    byte[] buf = new byte[800];
                    int bytesRead;
                    while ((bytesRead = ais.read(buf,0,buf.length)) != -1) {
                        //System.out.printf("Bytes read from txbuf: %d\n",
                        // bytesRead);
                        DatagramPacket packet = new DatagramPacket(buf,
                         bytesRead,new InetSocketAddress(
                         properties.getProperty("address",DEFAULT_ADDRESS),
                         Integer.parseInt(properties.getProperty("port",
                         DEFAULT_PORT))));
                        socket.send(packet);
                        //System.out.printf("Send packet length: %d\n",
                        // packet.getLength());
                        Thread.sleep(50);
                    }
                } catch (IOException|InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        });
        sendThread.setDaemon(true);

        // The receiveThread creates the receive DatagramSocket, reads the
        // inbound datagram packets from the other Intercom program  and writes
        // the data to the receive RingBuffer.
        receiveThread = new Thread(() -> {
            while (true) {
                try {
                    rxSocket = new DatagramSocket(Integer.parseInt(
                     properties.getProperty("port",DEFAULT_PORT)));
                    byte[] buf = new byte[1024];
                    DatagramPacket packet = new DatagramPacket(buf,buf.length);
                    while (true) {
                        rxSocket.receive(packet);
                        rxBuf.getOutputStream().write(packet.getData(),
                         packet.getOffset(),packet.getLength());
                        activity.update();
                        //System.out.printf("Receive packet length: %d\n",
                        // packet.getLength());
                        //System.out.println(
                         //rxBuf.getInputStream().available() +
                         //" - " + txBuf.getInputStream().available());
                    }
                } catch (IOException ioe) {
                    if (ioe instanceof SocketException)
                        System.out.println("Port Changed");
                    else
                        ioe.printStackTrace();
                } finally {
                    rxSocket.close();
                }
            }

        });
        receiveThread.setDaemon(true);

        // The audioThread reads the data in the receive RingBuffer, converts
        // the AudioFormat to PCM_SIGNED and writes that data to a
        // SourceDataLine.
        audioThread = new Thread(() -> {
            while (true) {
                try (AudioInputStream ais = new AudioInputStream(
                 rxBuf.getInputStream(),ULAW,AudioSystem.NOT_SPECIFIED);
                 AudioInputStream pcm =
                 AudioSystem.getAudioInputStream(PCM,ais);
                 SourceDataLine sdl = AudioSystem.getSourceDataLine(
                 pcm.getFormat())) {
                    //System.out.println(sdl.getFormat());
                    //System.out.println(sdl.getBufferSize());
                    //sdl.addLineListener(event ->
                     //System.out.printf("SDL: %s%n",event.getType()));
                    sdl.open();
                    sdl.start();

                    byte[] buf = new byte[800];
                    int bytesRead;
                    while ((bytesRead = pcm.read(buf)) != -1) {
                        sdl.write(buf,0,bytesRead);
                        //System.out.printf("Bytes read from rxbuf: %d\n",
                        // bytesRead);
                    }
                } catch (IOException|LineUnavailableException ex) {
                    ex.printStackTrace();
                }
            }
        });
        audioThread.setDaemon(true);
    }

    /**
     * Starts the three transfer threads.
     */
    public void start() {
        if (sendThread.getState() == Thread.State.NEW)
            sendThread.start();
        if (receiveThread.getState() == Thread.State.NEW)
            receiveThread.start();
        if (audioThread.getState() == Thread.State.NEW)
            audioThread.start();
    }

    /**
     * Reads data from the TargetDataLine (microphone) and writes that data to
     * the transmit RingBuffer when the talk button is pressed.
     */
    public void run() {
        try {
            tdl.start();
            byte[] buf = new byte[800];
            txBuf.getOutputStream().write(buf);
            int bytesRead;
            while (runFlag) {
                bytesRead = tdl.read(buf,0,buf.length);
                txBuf.getOutputStream().write(buf,0,bytesRead);
                //System.out.println(bytesRead);
            }
            Arrays.fill(buf,(byte)0);
            txBuf.getOutputStream().write(buf);
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } finally {
            tdl.stop();
            tdl.flush();
        }
    }

    /**
     * Loads the program properties from a data file
     *
     * @param   properties  destination for Properties read from file
     */
    private void loadProperties(Properties properties) {
        try (FileReader reader = new FileReader(PROPERTIES_FILE)) {
            properties.load(reader);
        } catch (IOException ioe) {
            JOptionPane.showMessageDialog(this,ioe,
             "Unable to Load Properties - Using Defaults",
             JOptionPane.WARNING_MESSAGE);
        }
    }

    /**
     * Stores the program properties to a data file
     *
     * @param   properties  source of Properties to be written to file
     */
    private void storeProperties(Properties properties) {
        try (FileWriter writer = new FileWriter(PROPERTIES_FILE)) {
            properties.store(writer,"Intercom");
        } catch (IOException ioe) {
            JOptionPane.showMessageDialog(this,ioe,
             "Unable to Store Properties",JOptionPane.WARNING_MESSAGE);
        }
    }

    /**
     * This RingBuffer is an unprotected circular buffer used to move byte data
     * to and from streams.
     */
    private static class RingBuffer {
        /** The RingBuffer's buffer */
        private final byte[] ringBuffer;

        /** An InputStream to read data from the RingBuffer */
        private final RingBuffer.InputStream is;

        /** An OutputStream to write data to the RingBuffer */
        private final RingBuffer.OutputStream os;

        /** The head of the buffer where data is read from first */
        private int head;

        /** The tail of the buffer where new data is written to the buffer */
        private int tail;

        /**
         * Creates a new RingBuffer of the specified size in bytes
         *
         * @param   bufSize number of bytes of space in the buffer
         */
        public RingBuffer(int bufSize) {
            ringBuffer = new byte[bufSize];
            is = new RingBuffer.InputStream();
            os = new RingBuffer.OutputStream();
        }

        /**
         * Gets the InputStream from this buffer
         *
         * @return  the buffer's InputStream
         */
        public RingBuffer.InputStream getInputStream() {
            return is;
        }

        /**
         * Gets this buffer's OutputStream
         *
         * @return  the buffer's OutputStream
         */
        public RingBuffer.OutputStream getOutputStream() {
            return os;
        }

        /**
         * An InputStream to read data from this buffer
         */
        class InputStream extends java.io.InputStream implements AutoCloseable {
            /** Flag to mark a closed stream */
            private volatile boolean closedFlag;

            /**
             *  Read a single byte from the buffer.  If there is no data in the
             *  buffer this code will block until data is available.
             *
             *  @return the byte read stored in an int
             *  @throws IOException if an attempt is made to read from a closed
             *          stream
             */
            @Override public int read() throws IOException {
                if (closedFlag)
                    throw new IOException("stream closed");

                synchronized (ringBuffer) {
                    while (head == tail)
                        try {
                            ringBuffer.wait();
                        } catch(InterruptedException ie) { }

                    int value = ringBuffer[head] & 0xff;
                
                    if (++head == ringBuffer.length)
                        head = 0;

                    return value;
                }
            }

            /**
             * Reads len bytes into the byte buffer starting at the off.
             *
             * @param   buf byte buffer to store the data that is read
             * @param   off offset within buffer where first byte is stored
             * @param   len number of bytes to attempt to read
             *
             * @return  number of bytes read and stored in buf
             *
             * @throws  IOException if an error occurs reading data
             * @throws  NullPointerException if buf is null
             * @throws  IndexOutOfBoundsException if off &lt; 0, len &lt; 0 or
             *          len &gt; buf.length - off
             */
            @Override public int read(byte[] buf, int off, int len) throws
             IOException {
                if (buf == null)
                    throw new NullPointerException("null buffer");
                if (off < 0 || len < 0 || len > buf.length - off)
                    throw new IndexOutOfBoundsException();

                if (len == 0)
                    return 0;

                int bytesRead = len;

                buf[off++] = (byte)read();
                --len;
                while (available() > 0 && len > 0) {
                    buf[off++] = (byte)read();
                    --len;
                }

                return bytesRead - len;
            }

            /**
             * Read bytes from the stream storing them into buf.  This is the
             * same as calling read(buf,0,buf.length)
             *
             * @return  number of bytes read and stored in buf
             *
             * @param   buf byte buffer to store read bytes
             *
             * @throws  IOException if an error occurs reading a byte
             */
            @Override public int read(byte[] buf) throws IOException {
                return read(buf,0,buf.length);
            }

            /**
             * Gets the number of bytes of data that may be read from the
             * InputStream.
             *
             * @return  number of bytes available to be read
             */
            @Override public int available() {
                synchronized (ringBuffer) {
                    if (head == tail)
                        return 0;

                    if (head < tail)
                        return tail - head;
                    else
                        return ringBuffer.length - head + tail;
                }
            }

            /**
             * Closes this stream
             *
             * @throws  IOException if an error occurs
             */
            @Override public void close() throws IOException {
                super.close();
                closedFlag = true;
            }
        }


        /**
         * An OutputStream to write data to this buffer
         */
        class OutputStream extends java.io.OutputStream implements
         AutoCloseable {
            /** Flag to mark a closed stream */
            private volatile boolean closedFlag;

            /**
             * Write a single byte to the buffer.
             *
             * @param   b byte to be written
             *
             * @throws  IOException if an error occurs
             */
            @Override public void write(int b) throws IOException {
                if (closedFlag)
                    throw new IOException("stream closed");

                synchronized (ringBuffer) {
                    ringBuffer[tail] = (byte)(b & 0xff);

                    if (++tail == ringBuffer.length)
                        tail = 0;
    
                    ringBuffer.notifyAll();
                }
            }

            /**
             * Write bytes from a byte array to this stream.
             *
             * @param   buf data to be written
             * @param   off index of first byte to be written
             * @param   len number of bytes to write
             *
             * @throws  IOException if an error occurs writing to the stream
             * @throws  NullPointerException if buf is null
             * @throws  IndexOutOfBoundsException if off &lt; 0, len &lt; 0 or
             *          off + len &gt; buf.length
             */
            @Override public void write(byte[] buf, int off, int len) throws
             IOException {
                if (buf == null)
                    throw new NullPointerException();
                if (off < 0 || len < 0 || off + len > buf.length)
                    throw new IndexOutOfBoundsException();

                synchronized (ringBuffer) {
                    for (int i=off; i<off + len; i++)
                        write(buf[i]);
                }
            }

            /**
             * Write the contents of the byte array to the buffer.  Same as
             * calling write(buf,0,len).
             *
             * @param   buf byte array to write to stream
             *
             * @throws  IOException if an error occurs
             */
            @Override public void write(byte[] buf) throws IOException {
                write(buf,0,buf.length);
            }

            /**
             * Closes this stream.
             *
             * @throws  IOException if an error occurs
             */
            @Override public void close() throws IOException {
                super.close();
                closedFlag = true;
            }
        }
    }

    /**
     * A JMenu used to signal an activity occuring in the program.
     */
    private static class ActivityJMenu extends JMenu {
        /** Serial version UID */
        private static final long serialVersionUID = 1L;

        /** Characters to display in the ActivityJMenu */
        private static final String[] text = { "|","/","-","\\" };

        /** Index of the next characther to display */
        private int index;

        /**
         * Create a new ActivityJMenu
         */
        public ActivityJMenu() {
            super(" ");
        }

        /**
         * Update the display by showing the next character
         */
        public void update() {
            EventQueue.invokeLater(() ->
             setText(text[index = ++index % text.length]));
        }
    }

    /**
     * Main program entry point, creates a new Intercom and starts it running.
     *
     * @param   args    command line arguments (not used)
     */
    public static void main(String... args) {
        EventQueue.invokeLater(() -> new Intercom().start());
    }
}


threeeye
Posts: 4
Joined: Thu Nov 30, 2023 3:47 pm

Re: DIY RPi 4 VoIP phone?

Thu Dec 07, 2023 5:01 pm

knute wrote:
Tue Dec 05, 2023 7:45 pm
I don't know what a softphone is but here you go:
A softphone is a SIP client that you install on a device (computer, Mobile phone (Android, IOS), etc...)
A SIP phone (hardphone) is a physical VoIP phone (Sangoma S500, Cisco 504G)

I think I'll just wait till my Rasperry Pi 5 will arrive, then I'll be able to run Android (my RPi4 is only 1GB RAM, and the min required to run Android is 2GB RAM)...

Thanks for your help and suggestions

Return to “Other projects”