Browse Source

first commit

master
asie 11 months ago
commit
1e4dc69353
9 changed files with 559 additions and 0 deletions
  1. 10
    0
      LICENSE
  2. 8
    0
      Makefile
  3. 22
    0
      README.md
  4. 87
    0
      lib/codec2.nim
  5. 115
    0
      src/audio.nim
  6. 269
    0
      src/main.nim
  7. 31
    0
      src/message.nim
  8. 16
    0
      src/message_test.nim
  9. 1
    0
      src/nim.cfg

+ 10
- 0
LICENSE View File

@@ -0,0 +1,10 @@
The MIT License (MIT)

Copyright (c) 2019 Adrian Siekierka

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


+ 8
- 0
Makefile View File

@@ -0,0 +1,8 @@
all: voirc

voirc: src/main.nim src/audio.nim src/message.nim lib/codec2.nim
nim c --threads:on --tlsEmulation:off --gc:stack -d:release src/main.nim
mv src/main voirc

clean:
rm voirc

+ 22
- 0
README.md View File

@@ -0,0 +1,22 @@
# VoIRC

The Voice over IRC client.

The code is terrible. Sorry. It might get better.

## Requirements

* Nim (tested with 0.19.2)
* GNU make
* Unicode-compliant ncurses
* codec2 0.8.1 ([source](https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/codec2/0.8.1-2/codec2_0.8.1.orig.tar.xz))
* libsoundio 1.1.0 ([source](https://github.com/andrewrk/libsoundio/archive/1.1.0.tar.gz))
* The following Nim packages (install with "nimble install"):
* irc
* ncurses
* soundio

## Installation

1. Run "make".
2. Run "./voirc [IRC server](:port) [nickname] '[#channel]'.

+ 87
- 0
lib/codec2.nim View File

@@ -0,0 +1,87 @@
{.deadCodeElim: on.}
when defined(windows):
const
codec2dll* = "codec2.dll"
elif defined(macosx):
const
codec2dll* = "libcodec2.dylib"
else:
const
codec2dll* = "libcodec2.so"
## ---------------------------------------------------------------------------*\
##
## FILE........: codec2.h
## AUTHOR......: David Rowe
## DATE CREATED: 21 August 2010
##
## Codec 2 fully quantised encoder and decoder functions. If you want use
## Codec 2, these are the functions you need to call.
##
## \*---------------------------------------------------------------------------
##
## Copyright (C) 2010 David Rowe
##
## All rights reserved.
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU Lesser General Public License version 2.1, as
## published by the Free Software Foundation. This program is
## distributed in the hope that it will be useful, but WITHOUT ANY
## WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
## License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, see <http://www.gnu.org/licenses/>.
##

const
CODEC2_VERSION_MAJOR* = 0
CODEC2_VERSION_MINOR* = 8
CODEC2_VERSION_PATCH* = 1
CODEC2_VERSION* = "0.8.1"

const
CODEC2_MODE_3200* = 0
CODEC2_MODE_2400* = 1
CODEC2_MODE_1600* = 2
CODEC2_MODE_1400* = 3
CODEC2_MODE_1300* = 4
CODEC2_MODE_1200* = 5
CODEC2_MODE_700* = 6
CODEC2_MODE_700B* = 7
CODEC2_MODE_700C* = 8
CODEC2_MODE_WB* = 9

type
CODEC2* {.bycopy.} = object


proc codec2_create*(mode: cint): ptr CODEC2 {.cdecl, importc: "codec2_create",
dynlib: codec2dll.}
proc codec2_destroy*(codec2_state: ptr CODEC2) {.cdecl, importc: "codec2_destroy",
dynlib: codec2dll.}
proc codec2_encode*(codec2_state: ptr CODEC2; bits: ptr cuchar; speech_in: ptr cshort) {.
cdecl, importc: "codec2_encode", dynlib: codec2dll.}
proc codec2_decode*(codec2_state: ptr CODEC2; speech_out: ptr cshort; bits: ptr cuchar) {.
cdecl, importc: "codec2_decode", dynlib: codec2dll.}
proc codec2_decode_ber*(codec2_state: ptr CODEC2; speech_out: ptr cshort;
bits: ptr cuchar; ber_est: cfloat) {.cdecl,
importc: "codec2_decode_ber", dynlib: codec2dll.}
proc codec2_samples_per_frame*(codec2_state: ptr CODEC2): cint {.cdecl,
importc: "codec2_samples_per_frame", dynlib: codec2dll.}
proc codec2_bits_per_frame*(codec2_state: ptr CODEC2): cint {.cdecl,
importc: "codec2_bits_per_frame", dynlib: codec2dll.}
proc codec2_set_lpc_post_filter*(codec2_state: ptr CODEC2; enable: cint;
bass_boost: cint; beta: cfloat; gamma: cfloat) {.
cdecl, importc: "codec2_set_lpc_post_filter", dynlib: codec2dll.}
proc codec2_get_spare_bit_index*(codec2_state: ptr CODEC2): cint {.cdecl,
importc: "codec2_get_spare_bit_index", dynlib: codec2dll.}
proc codec2_rebuild_spare_bit*(codec2_state: ptr CODEC2; unpacked_bits: ptr cint): cint {.
cdecl, importc: "codec2_rebuild_spare_bit", dynlib: codec2dll.}
proc codec2_set_natural_or_gray*(codec2_state: ptr CODEC2; gray: cint) {.cdecl,
importc: "codec2_set_natural_or_gray", dynlib: codec2dll.}
proc codec2_set_softdec*(c2: ptr CODEC2; softdec: ptr cfloat) {.cdecl,
importc: "codec2_set_softdec", dynlib: codec2dll.}
proc codec2_get_energy*(codec2_state: ptr CODEC2; bits: ptr cuchar): cfloat {.cdecl,
importc: "codec2_get_energy", dynlib: codec2dll.}

+ 115
- 0
src/audio.nim View File

@@ -0,0 +1,115 @@
import sequtils
import soundio

var audioReaderFunc: proc(buffer: var seq[int16])
var audioWriterFunc: proc(frameCountMin: int, frameCountMax: int): seq[int16]
var audioBuffer = newSeq[int16](0)
var sio: ptr SoundIo

proc readCallback(inStream: ptr SoundIoInStream, frameCountMin: cint, frameCountMax: cint) {.cdecl.} =
var areas: ptr SoundIoChannelArea
var framesLeft = frameCountMax
while true:
var frameCount = framesLeft
var err = inStream.begin_read(areas.addr, frameCount.addr)
if frameCount <= 0:
break
for frame in 0..<frameCount:
var ptrSample = cast[ptr int16](cast[int](areas.pointer) + frame*areas.step)
audioBuffer.add(ptrSample[])
err = inStream.end_read()
framesLeft -= frameCount
if framesLeft <= 0:
break
audioReaderFunc(audioBuffer)

proc writeCallback(outStream: ptr SoundIoOutStream, frameCountMin: cint, frameCountMax: cint) {.cdecl.} =
let data = audioWriterFunc(frameCountMin, frameCountMax)
var areas: ptr SoundIoChannelArea
var framesLeft = frameCountMax
var framePos = 0
while true:
var frameCount = framesLeft
var err = outStream.beginWrite(areas.addr, frameCount.addr)
if frameCount <= 0:
break
let layout = outStream.layout
for frame in 0..<frameCount:
let sample = data[framePos + frame]
for channel in 0..<layout.channelCount:
let ptrArea = cast[ptr SoundIoChannelArea](cast[int](areas) + channel * (sizeof SoundIoChannelArea))
var ptrSample = cast[ptr int16](cast[int](ptrArea.pointer) + frame*ptrArea.step)
ptrSample[] = int16(sample)
err = outStream.endWrite()
framesLeft -= frameCount
framePos += frameCount
if framesLeft <= 0:
break

proc init*() =
sio = soundioCreate()
if sio.isNil:
quit "out of memory"
if sio.connect() > 0:
quit "unable to connect to backend"
echo "Backend: ", sio.currentBackend.name
sio.flushEvents()

proc init_record*(sampleRate: int, afunc: proc(buffer: var seq[int16])) =
audioReaderFunc = afunc
let devID = sio.defaultInputDeviceIndex
let microphone = sio.getInputDevice(devID)
if microphone.isNil:
quit "out of memory"
if microphone.probeError > 0:
quit "unable to connect to device"

echo "Microphone: ", microphone.name
let micStream = microphone.inStreamCreate()
micStream.format = SoundIoFormatS16NE
micStream.sample_rate = cast[cint](sampleRate)
micStream.read_callback = readCallback
let err = micStream.open()
if err > 0:
quit "unable to start listening (1)" & $err
if micStream.layoutError > 0:
quit "unable to start listening (2)" & $micStream.layoutError
if micStream.start() > 0:
quit "unable to start listening (3)"

proc init_playback*(sampleRate: int, afunc: proc(frameCountMin: int, frameCountMax: int): seq[int16]) =
audioWriterFunc = afunc
let devID = sio.defaultOutputDeviceIndex
let speaker = sio.getOutputDevice(devID)
if speaker.isNil:
quit "out of memory"
if speaker.probeError > 0:
quit "unable to connect to device"

echo "Speaker: ", speaker.name
let outStream = speaker.outStreamCreate()
outStream.format = SoundIoFormatS16NE
outStream.sample_rate = cast[cint](sampleRate)
outStream.write_callback = writeCallback
let err = outStream.open()
if err > 0:
quit "unable to start playing (1)" & $err
if outStream.layoutError > 0:
quit "unable to start playing (2)" & $outStream.layoutError.strerror
if outStream.start() > 0:
quit "unable to start playing (3)"

proc update*() =
sio.flushEvents()

proc deinit*() =
sio.destroy()

+ 269
- 0
src/main.nim View File

@@ -0,0 +1,269 @@
import asyncdispatch
import audio
import codec2
import critbits
import irc
import locks
import message
import ncurses
import os
import sequtils
import strutils
import times

const BITS_PER_SECOND = 1200
const BITS_PER_MESSAGE = 2496
# BITS_PER_MESSAGE / 8
const BYTES_PER_MESSAGE = 312
const SAMPLE_RATE = 8000
# SAMPLE_RATE * BITS_PER_MESSAGE / BITS_PER_SECOND
const BUFFER_SIZE = 16640

type
ReceivedAudio = ref object of RootObj
id: string
frames: seq[int16]

var recordingAudio = false
var lastRecordingAudio = false
var audioTrackerModifyLock: Lock
initLock(audioTrackerModifyLock)

let arguments = commandLineParams()
var stdscr, wtext, wstatus, wfield: ptr window

var currServerSplit = split(arguments[0], {':'})
var currPort = Port(6667)
var currNickname = arguments[1]
var currChannel = arguments[2]
var client: AsyncIrc
var msgCnt = 0
var audioTracker = newSeq[var ReceivedAudio](0)

if currServerSplit.len >= 2:
currPort = Port(parseInt(currServerSplit[1]))
var currServer = currServerSplit[0]

audio.init()

proc finishMain() {.noconv.} =
audio.deinit()
endwin()

addQuitProc(finishMain)

stdscr = initscr()
cbreak()
noecho()
scrollok(wtext, true)
nodelay(stdscr, true)
keypad(stdscr, true)

let codec = codec2Create(CODEC2_MODE_1200)
let samplesPerFrame = codec2_samples_per_frame(codec)
let bytesPerFrame = (codec2_bits_per_frame(codec) + 7) div 8

proc showMsg(msg: string) =
waddstr(wtext, cstring(msg))
wrefresh(wtext)

proc updateStatusbar() =
werase(wstatus)
wmove(wstatus, 0, 0)
waddstr(wstatus, if recordingAudio: "[REC] " else: "[ ] ")
var names: CritBitTree[void]
for i in 0..<audioTracker.len:
let id = audioTracker[i].id
if audioTracker[i].frames.len > 0:
names.incl(id[8..<id.len])
var i = 0
for name in names.keys:
if i > 0:
waddstr(wstatus, ", ")
waddstr(wstatus, name)
i += 1
wrefresh(wstatus)

proc findAudioTracker(key: string): var ReceivedAudio =
for i in 0..audioTracker.high:
if audioTracker[i].id == key:
return audioTracker[i]
add(audioTracker, ReceivedAudio(id: key, frames: newSeq[int16](0)))
return audioTracker[audioTracker.high]

proc calcPrivmsgSpace(nick: string, channel: string): int =
var l = 512
# ":[nick]![host] "
l -= (3 + nick.len + 63)
# "PRIVMSG [channel] :"
l -= (10 + channel.len)
return l

proc decodeFrame(sender: string, msg: string): bool =
if msg.len < 8:
return false
let msgHeader = msg[0..7]
let msgHeaderLower = toLower(msgHeader)
if msgHeaderLower != "voirc01]":
return false
let msgEnc = msg[8..<msg.len]
let msgKey = msgHeader & sender
var msgDec = message.decode(msgEnc)
var outBuf: array[SAMPLE_RATE, cshort]
var frameHolder = findAudioTracker(msgKey)
var inPos = 0
var changed = frameHolder.frames.len == 0
while inPos < msgDec.len:
codec2_decode(codec, cast[ptr cshort](outBuf[0].addr), cast[ptr cuchar](msgDec[inPos].addr))
acquire(audioTrackerModifyLock)
for i in 0..<samplesPerFrame:
add(frameHolder.frames, int16(outBuf[i]))
release(audioTrackerModifyLock)
inPos += bytesPerFrame
if changed:
updateStatusbar()
return true

proc writeCallback(fcMin: int, fcMax: int): seq[int16] =
var outSeq = newSeq[int16](0)
if fcMax < 1:
return outSeq
for i in 0..<fcMax:
var sample = int16(0)
for j in 0..<audioTracker.len:
let tracker = audioTracker[j]
if tracker.frames.high >= i:
sample += tracker.frames[i]
add(outSeq, sample)
var changed = false
acquire(audioTrackerModifyLock)
for j in 0..<audioTracker.len:
if audioTracker[j].frames.len > 0:
delete(audioTracker[j].frames, 0, min(audioTracker[j].frames.high, fcMax-1))
changed = changed or (audioTracker[j].frames.len == 0)
release(audioTrackerModifyLock)
if changed:
updateStatusbar()
return outSeq

proc sendFrame(audioBuffer: var seq[int16]) =
let maxFramePos = min(BUFFER_SIZE, audioBuffer.len)
var frame: array[BUFFER_SIZE, cshort]
var output: array[BYTES_PER_MESSAGE, cuchar]
for i in 0..<BUFFER_SIZE:
if audioBuffer.high >= i:
frame[i] = audioBuffer[i]
else:
frame[i] = 0
audioBuffer.delete(0, maxFramePos - 1)
var outPos = 0
var framePos = 0
while framePos < maxFramePos:
codec2_encode(codec, cast[ptr cuchar](output[outPos].addr), cast[ptr cshort](frame[framePos].addr))
framePos += samplesPerFrame
outPos += bytesPerFrame
var msgCntStr = ""
add(msgCntStr, if (msgCnt and 4) != 0: 'I' else: 'i')
add(msgCntStr, if (msgCnt and 2) != 0: 'R' else: 'r')
add(msgCntStr, if (msgCnt and 1) != 0: 'C' else: 'c')
var msg = "Vo" & msgCntStr & "01]" & message.encode(output, outPos)
var pmFuture = client.privmsg(currChannel, msg)

proc readCallback(audioBuffer: var seq[int16]) =
if lastRecordingAudio or recordingAudio:
if (audioBuffer.len >= BUFFER_SIZE) or (not recordingAudio):
sendFrame(audioBuffer)
else:
delete(audioBuffer, 0, audioBuffer.high)
lastRecordingAudio = recordingAudio

proc ircCallback(client: AsyncIrc, event: IrcEvent) {.async.} =
case event.typ
of EvConnected:
return
of EvDisconnected, EvTimeout:
return
of EvMsg:
if event.cmd == MPrivMsg:
if not decodeFrame(event.nick, event.params[event.params.high]):
showMsg("\n[" & event.origin & "] <" & event.nick & "> " & event.params[event.params.high])
elif event.cmd == MPong:
return
else:
showMsg("\n" & event.raw)

var scrw, scrh: int
var lastw = 0
var lasth = 0

getmaxyx(stdscr, scrh, scrw)
wtext = newwin(scrh - 2, scrw, 0, 0)
wstatus = newwin(1, scrw, scrh - 2, 0)
wfield = newwin(1, scrw, scrh - 1, 0)
lastw = scrw
lasth = scrh

proc processKeypresses() {.async.} =
var msgBuf = ""
while true:
var c = wgetch(stdscr)
if c < 0:
await sleepAsync(20)
elif c == 10 or c == 13:
if msgBuf.len > 0 and not recordingAudio:
var pmfuture = client.privmsg(currChannel, msgBuf)
showMsg("\n[" & currChannel & "] <" & currNickname & "> " & msgBuf)
msgBuf = ""
werase(wfield)
elif c >= 32 and c <= 127:
add(msgBuf, char(c))
waddstr(wfield, $char(c))
wrefresh(wfield)
elif c == 263:
if msgBuf.len > 0:
msgBuf = msgBuf[0..(msgBuf.high-1)]
var cx, cy: int
getyx(wfield, cy, cx)
if cx >= 1:
cx -= 1
mvwaddch(wfield, cy, cx, 32)
wmove(wfield, cy, cx)
wrefresh(wfield)
elif c == 276:
recordingAudio = not recordingAudio
if recordingAudio:
msgCnt += 1
updateStatusbar()
elif c == 410:
getmaxyx(stdscr, scrh, scrw)
if (scrw != lastw) or (scrh != lasth):
mvwin(wtext, 0, 0)
wresize(wtext, scrh - 2, scrw)
mvwin(wstatus, scrh - 2, 0)
wresize(wstatus, 1, scrw)
mvwin(wfield, scrh - 1, 0)
wresize(wfield, 1, scrw)
lastw = scrw
lasth = scrh
wrefresh(wtext)
wrefresh(wstatus)
wrefresh(wfield)
else:
showMsg("\nUnknown key: " & $c)

proc processAudio() {.async.} =
while true:
audio.update()
await sleepAsync(40)

audio.init_record(SAMPLE_RATE, readCallback)
audio.init_playback(SAMPLE_RATE, writeCallback)

client = newAsyncIrc(currServer, port=currPort, nick=currNickname, joinChans = @[currChannel], callback = ircCallback)
asyncCheck client.run()
asyncCheck processAudio()

asyncCheck processKeypresses()

updateStatusbar()
runForever()

+ 31
- 0
src/message.nim View File

@@ -0,0 +1,31 @@
proc encode*(inArray: openArray[cuchar], maxLen: int): string =
var outString = ""
var inValue = int(0)
var inBits = int(0)
# turn every 13 bits into an output char pair. that's the spirit!
for i in 0..<maxLen:
inValue = inValue or (int(inArray[i]) shl inBits)
inBits += 8
if (inBits >= 13) or (i == inArray.high):
let chValue = (inValue and 0x1FFF)
inValue = inValue shr 13
inBits -= 13
add(outString, char(33 + (chValue div 94)))
add(outString, char(33 + (chValue mod 94)))
return outString

proc decode*(inString: string): seq[cuchar] =
var outSeq = newSeq[cuchar](0)
var outValue = int(0)
var outBits = int(0)
if (inString.len mod 2) == 1:
return outSeq
for i in countup(0,inString.len-1,2):
var tmpValue = ((int(inString[i])-33)*94) + (int(inString[i+1])-33)
outValue = outValue or (tmpValue shl outBits)
outBits += 13
while (outBits >= 8):
add(outSeq, cuchar(outValue and 0xFF))
outValue = outValue shr 8
outBits -= 8
return outSeq

+ 16
- 0
src/message_test.nim View File

@@ -0,0 +1,16 @@
import message

const BYTES_PER_MESSAGE = 312

var input = newSeq[cuchar](0)
for i in 0..<BYTES_PER_MESSAGE:
add(input, cuchar((i and 0x1E) + 67))

let tmp = message.encode(input, input.len)
let output = message.decode(tmp)

for i in 0..<BYTES_PER_MESSAGE:
if input[i] != output[i]:
echo("MISMATCH @ ", i, ": ", input[i], " != ", output[i])
else:
echo("MATCH @ ", i, ": ", input[i], " == ", output[i])

+ 1
- 0
src/nim.cfg View File

@@ -0,0 +1 @@
path="../lib"

Loading…
Cancel
Save