$ git clone http://tcclient.ion.nu/tc_client.git
commit 5f2908b53e792242ca6006611bb91b4f45d92c22
Author: Alicia <...>
Date:   Tue Apr 7 06:49:01 2015 +0200

    Version 0.26

diff --git a/ChangeLog b/ChangeLog
index 317ada6..9dfa01b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+0.26:
+modbot: fixed a segfault when trying to skip more videos than what's in the queue.
+modbot: fixed a segfault when receiving a manual /mbc command but no video is playing.
+cursedchat: added PM buffers/windows which can be switched between with "/pm <name>", just "/pm" to return to the channel buffer.
+cursedchat: added /help output specific to cursedchat, in addition to tc_client's output.
+camviewer: added an experimental audio mixer.
 0.25:
 Handle short reads in the RTMP headers too.
 Added the option -c/--color <number> to pick color instead of getting a random color at startup (contributed by Pamela)
diff --git a/Makefile b/Makefile
index 0f7e575..da7c8da 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.25
+VERSION=0.26
 CFLAGS=-g3 -Wall $(shell curl-config --cflags)
 LIBS=-g3 $(shell curl-config --libs)
 ifneq ($(wildcard config.mk),)
@@ -8,7 +8,7 @@ OBJ=client.o amfparser.o rtmp.o numlist.o amfwriter.o idlist.o colors.o endian.o
 IRCHACK_OBJ=utilities/irchack/irchack.o
 MODBOT_OBJ=utilities/modbot/modbot.o utilities/modbot/list.o utilities/modbot/queue.o
 CAMVIEWER_OBJ=utilities/camviewer/camviewer.o
-CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o
+CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o utilities/cursedchat/buffer.o
 UTILS=irchack modbot
 ifdef GTK_LIBS
 ifdef AVCODEC_LIBS
@@ -53,4 +53,4 @@ clean:
  rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) tc_client irchack modbot camviewer cursedchat
 
 tarball:
- tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/list.c utilities/modbot/list.h utilities/modbot/queue.c utilities/modbot/queue.h utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c configure
+ tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/list.c utilities/modbot/list.h utilities/modbot/queue.c utilities/modbot/queue.h utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h configure
diff --git a/utilities/camviewer/camviewer.c b/utilities/camviewer/camviewer.c
index 1f9330c..43a8094 100644
--- a/utilities/camviewer/camviewer.c
+++ b/utilities/camviewer/camviewer.c
@@ -45,6 +45,8 @@ struct camera
   GtkWidget* cam;
   AVCodecContext* vctx;
   AVCodecContext* actx;
+  short* samples;
+  unsigned int samplecount;
   char* id;
   char* nick;
   GtkWidget* box; // holds label and cam
@@ -59,12 +61,44 @@ struct viddata
   AVCodec* adecoder;
 #ifdef HAVE_SOUND
   int audiopipe;
+  SwrContext* swrctx;
 #endif
 };
 
 int tc_client[2];
 int tc_client_in[2];
 
+#ifdef HAVE_SOUND
+// Experimental mixer, not sure if it really works
+void camera_playsnd(struct viddata* data, struct camera* cam, short* samples, unsigned int samplecount)
+{
+  if(cam->samples)
+  {
+// int sources=1;
+    unsigned int i;
+    for(i=0; i<data->camcount; ++i)
+    {
+      if(!data->cams[i].samples){continue;}
+      if(cam==&data->cams[i]){continue;}
+      unsigned j;
+      for(j=0; j<cam->samplecount && j<data->cams[i].samplecount; ++j)
+      {
+        cam->samples[j]+=data->cams[i].samples[j];
+      }
+      free(data->cams[i].samples);
+      data->cams[i].samples=0;
+// ++sources;
+    }
+    write(data->audiopipe, cam->samples, cam->samplecount*sizeof(short));
+    free(cam->samples);
+// printf("Mixed sound from %i sources (cam: %p)\n", sources, cam);
+  }
+  cam->samples=malloc(samplecount*sizeof(short));
+  memcpy(cam->samples, samples, samplecount*sizeof(short));
+  cam->samplecount=samplecount;
+}
+#endif
+
 char buf[1024];
 gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
 {
@@ -120,6 +154,7 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
 #ifdef HAVE_SOUND
     cam->actx=avcodec_alloc_context3(data->adecoder);
     avcodec_open2(cam->actx, data->adecoder, 0);
+    cam->samples=0;
 #endif
     cam->cam=gtk_image_new();
     cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
@@ -181,11 +216,8 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     int gotframe;
     avcodec_decode_audio4(cam->actx, cam->frame, &gotframe, &pkt);
     if(!gotframe){return 1;}
-    SwrContext* swrctx=swr_alloc_set_opts(0, 1, AV_SAMPLE_FMT_S16, 22050, 1, cam->frame->format, 11025, 0, 0); // TODO: any way to get the sample rate from the frame/decoder? cam->frame->sample_rate seems to be 0
-    swr_init(swrctx);
-    int outlen=swr_convert(swrctx, cam->frame->data, cam->frame->nb_samples, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
-    swr_free(&swrctx);
-    write(data->audiopipe, cam->frame->data[0], outlen);
+    int outlen=swr_convert(data->swrctx, cam->frame->data, cam->frame->nb_samples, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
+    camera_playsnd(data, cam, (short*)cam->frame->data[0], outlen);
 #endif
     return 1;
   }
@@ -253,7 +285,6 @@ void audiothread(int fd)
   samplefmt.matrix=0;
   ao_option clientname={.key="client_name", .value="tc_client/camviewer", .next=0};
   ao_device* dev=ao_open_live(ao_default_driver_id(), &samplefmt, &clientname);
-// TODO: mix sounds, somehow..
   char buf[2048];
   size_t len;
   while((len=read(fd, buf, 2048))>0)
@@ -272,6 +303,8 @@ int main(int argc, char** argv)
   data.adecoder=avcodec_find_decoder(AV_CODEC_ID_NELLYMOSER);
 
 #ifdef HAVE_SOUND
+  data.swrctx=swr_alloc_set_opts(0, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_S16, 22050, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_FLT, 11025, 0, 0); // TODO: any way to get the sample rate from the frame/decoder? cam->frame->sample_rate seems to be 0
+  swr_init(data.swrctx);
   int audiopipe[2];
   pipe(audiopipe);
   data.audiopipe=audiopipe[1];
@@ -319,6 +352,7 @@ int main(int argc, char** argv)
     avcodec_free_context(&data.cams[i].vctx);
 #ifdef HAVE_SOUND
     avcodec_free_context(&data.cams[i].actx);
+    swr_free(&data.swrctx);
 #endif
     free(data.cams[i].id);
     free(data.cams[i].nick);
diff --git a/utilities/cursedchat/buffer.c b/utilities/cursedchat/buffer.c
new file mode 100644
index 0000000..af06a16
--- /dev/null
+++ b/utilities/cursedchat/buffer.c
@@ -0,0 +1,62 @@
+/*
+    cursedchat, a simple curses interface for tc_client
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    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 Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <string.h>
+#include <stdlib.h>
+#include <curses.h>
+#include "buffer.h"
+
+struct buffer* buffers=0;
+unsigned int buffercount=0;
+unsigned int currentbuf=0;
+
+unsigned int createbuffer(const char* name)
+{
+  ++buffercount;
+  buffers=realloc(buffers, buffercount*sizeof(struct buffer));
+  buffers[buffercount-1].pad=newpad(2048, COLS);
+  scrollok(buffers[buffercount-1].pad, 1);
+  buffers[buffercount-1].name=(name?strdup(name):0);
+  buffers[buffercount-1].scroll=-1;
+  buffers[buffercount-1].seen=1;
+  return buffercount-1;
+}
+
+unsigned int findbuffer(const char* name)
+{
+  unsigned int i;
+  for(i=1; i<buffercount; ++i)
+  {
+    if(!strcmp(buffers[i].name, name)){return i;}
+  }
+  return 0;
+}
+
+void renamebufferunique(unsigned int id)
+{
+  unsigned int i=0;
+  while(i<buffercount)
+  {
+    int len=strlen(buffers[id].name);
+    buffers[id].name=realloc(buffers[id].name, len+2);
+    buffers[id].name[len]='_';
+    buffers[id].name[len+1]=0;
+    for(i=1; i<buffercount; ++i)
+    {
+      if(i!=id && !strcmp(buffers[i].name, buffers[id].name)){break;}
+    }
+  }
+}
diff --git a/utilities/cursedchat/buffer.h b/utilities/cursedchat/buffer.h
new file mode 100644
index 0000000..79ec1fc
--- /dev/null
+++ b/utilities/cursedchat/buffer.h
@@ -0,0 +1,31 @@
+/*
+    cursedchat, a simple curses interface for tc_client
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    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 Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+struct buffer
+{
+  WINDOW* pad;
+  char* name;
+  int scroll;
+  char seen;
+};
+
+extern struct buffer* buffers;
+extern unsigned int buffercount;
+extern unsigned int currentbuf;
+
+extern unsigned int createbuffer(const char* name);
+extern unsigned int findbuffer(const char* name);
+extern void renamebufferunique(unsigned int id);
diff --git a/utilities/cursedchat/cursedchat.c b/utilities/cursedchat/cursedchat.c
index fe00302..42545f5 100644
--- a/utilities/cursedchat/cursedchat.c
+++ b/utilities/cursedchat/cursedchat.c
@@ -22,12 +22,13 @@
 #include <locale.h>
 #include <curses.h>
 #include <readline/readline.h>
+#include "buffer.h"
 
 #define HALFSCREEN (LINES>4?(LINES-3)/2:1)
+
 WINDOW* topic;
-WINDOW* chat;
+char* channeltopic;
 WINDOW* input;
-int chatscroll=-1;
 int to_app;
 
 // Translate ANSI escape codes to curses commands and write the text to a window
@@ -75,18 +76,114 @@ void waddansi(WINDOW* w, char* str)
 
 void drawchat(void)
 {
-  prefresh(chat, (chatscroll>-1?chatscroll:getcury(chat)-LINES+4), 0, 1, 0, LINES-3, COLS);
+  WINDOW* w=buffers[currentbuf].pad;
+  int scroll=buffers[currentbuf].scroll;
+  prefresh(w, (scroll>-1?scroll:getcury(w)-LINES+4), 0, 1, 0, LINES-3, COLS);
+}
+
+void drawtopic(void)
+{
+  werase(topic);
+  unsigned int i;
+  for(i=1; i<buffercount && buffers[i].seen; ++i);
+  if(i<buffercount)
+  {
+    waddstr(topic, "Unread PMs from: ");
+    char first=1;
+    for(i=1; i<buffercount; ++i)
+    {
+      if(!buffers[i].seen)
+      {
+        if(first){first=0;}else{waddstr(topic, ", ");}
+        waddstr(topic, buffers[i].name);
+      }
+    }
+  }
+  else if(currentbuf)
+  {
+    waddstr(topic, "To return to public chat type: /pm");
+  }else{
+    waddstr(topic, channeltopic);
+  }
+  wrefresh(topic);
 }
 
 void gotline(char* line)
 {
   if(!line){close(to_app); return;} // TODO: handle EOF on stdin better?
-// TODO: handle commands (/pm, /help addition)
+  if(!strcmp(line, "/pm"))
+  {
+    currentbuf=0;
+    drawchat();
+    drawtopic();
+    return;
+  }
+  else if(!strncmp(line, "/pm ", 4))
+  {
+    currentbuf=findbuffer(&line[4]);
+    if(!currentbuf){currentbuf=createbuffer(&line[4]);}
+    buffers[currentbuf].seen=1;
+    drawchat();
+    drawtopic();
+    return;
+  }
+  else if(!strncmp(line, "/buffer ", 8))
+  {
+    unsigned int num=atoi(&line[8]);
+    if(num<0 || num>=buffercount)
+    {
+      wprintw(buffers[currentbuf].pad, "\nInvalid buffer number: %u", num);
+    }else{
+      currentbuf=num;
+      buffers[currentbuf].seen=1;
+      drawtopic();
+    }
+    drawchat();
+    return;
+  }
+  else if(!strcmp(line, "/bufferlist"))
+  {
+    unsigned int i;
+    for(i=0; i<buffercount; ++i)
+    {
+      wprintw(buffers[currentbuf].pad, "\n% 3i: %s", i, i?buffers[i].name:"");
+    }
+    drawchat();
+    return;
+  }
+  else if(!strncmp(line, "/msg ", 5))
+  {
+    char* name=&line[5];
+    char* msg=strchr(name, ' ');
+    if(!msg){return;}
+    msg[0]=0;
+    currentbuf=findbuffer(name);
+    if(!currentbuf){currentbuf=createbuffer(name);}
+    buffers[currentbuf].seen=1;
+    drawtopic();
+    memmove(line, &msg[1], strlen(&msg[1])+1);
+  }
+  else if(!strcmp(line, "/help"))
+  {
+    waddstr(buffers[0].pad, "\nFor cursedchat:\n"
+      "/pm <name>    = switch to the PM buffer for <name>\n"
+      "/pm           = return to the channel/public chat's buffer\n"
+      "/buffer <num> = switch to buffer by number\n"
+      "/bufferlist   = list open buffers, their numbers and associated names\n"
+      "\nFor tc_client (through cursedchat):");
+    write(to_app, line, strlen(line));
+    write(to_app, "\n", 1);
+    return;
+  }
 
+  if(currentbuf) // We're in a PM window, make the message a PM
+  {
+    dprintf(to_app, "/msg %s ", buffers[currentbuf].name);
+  }
   write(to_app, line, strlen(line));
   write(to_app, "\n", 1);
 // TODO: grab user's nick for this
-  wprintw(chat, "\n%s: %s", "You", line);
+  wprintw(buffers[currentbuf].pad, "\n%s: %s", "You", line);
   drawchat();
 }
 
@@ -127,22 +224,23 @@ int escinput(int a, int byte)
   if(!strcmp(buf, "[5")) // Page up
   {
     read(0, buf, 1);
-    if(chatscroll<0){chatscroll=getcury(chat)-LINES+4;}
-    chatscroll-=HALFSCREEN; // (LINES-3)/2;
-    if(chatscroll<0){chatscroll=0;}
+    struct buffer* b=&buffers[currentbuf];
+    if(b->scroll<0){b->scroll=getcury(b->pad)-LINES+4;}
+    b->scroll-=HALFSCREEN;
+    if(b->scroll<0){b->scroll=0;}
     drawchat();
     return 0;
   }
   if(!strcmp(buf, "[6")) // Page down
   {
     read(0, buf, 1);
-    if(chatscroll<0){return 0;} // Already at the bottom
-    chatscroll+=HALFSCREEN; // (LINES-3)/2;
-    if(chatscroll>getcury(chat)-LINES+3){chatscroll=-1;}
+    struct buffer* b=&buffers[currentbuf];
+    if(b->scroll<0){return 0;} // Already at the bottom
+    b->scroll+=HALFSCREEN;
+    if(b->scroll>getcury(b->pad)-LINES+3){b->scroll=-1;}
     drawchat();
     return 0;
   }
-//  wprintw(chat, "\nbuf: %s", buf);
   return 0;
 }
 
@@ -168,15 +266,21 @@ void resizechat(int sig)
   ioctl(0, TIOCGWINSZ, &size);
   if(size.ws_row<3){return;} // Too small, would result in negative numbers breaking the chat window
   resize_term(size.ws_row, size.ws_col);
+  clear();
+  refresh();
   wresize(topic, 1, COLS);
-  wresize(chat, chat->_maxy+1, COLS);
+  unsigned int i;
+  for(i=0; i<buffercount; ++i)
+  {
+    wresize(buffers[i].pad, buffers[i].pad->_maxy+1, COLS);
+  }
   wresize(input, 2, COLS);
   mvwin(input, LINES-2, 0);
-  redrawwin(chat);
+  redrawwin(buffers[currentbuf].pad);
   redrawwin(topic);
   redrawwin(input);
   drawchat();
-  wrefresh(topic);
+  drawtopic();
   drawinput();
 }
 
@@ -203,8 +307,7 @@ int main(int argc, char** argv)
   init_pair(7, COLOR_CYAN, -1);
 
   wbkgd(topic, COLOR_PAIR(1)|' ');
-  chat=newpad(2048, COLS);
-  scrollok(chat, 1);
+  createbuffer(0);
   input=newwin(2, COLS, LINES-2, 0);
   scrollok(input, 1);
   rl_initialize();
@@ -248,14 +351,61 @@ int main(int argc, char** argv)
       }
       if(len==-1){break;} // Bad read
       buf[len]=0;
+      unsigned int buffer=0;
       if(!strncmp(buf, "Room topic: ", 12))
       {
-        werase(topic);
-        waddstr(topic, &buf[12]);
-        wrefresh(topic);
+        free(channeltopic);
+        channeltopic=strdup(&buf[12]);
+        drawtopic();
+      }
+      else if(buf[0]=='['&&isdigit(buf[1])&&isdigit(buf[2])&&buf[3]==':'&&isdigit(buf[4])&&isdigit(buf[5])&&buf[6]==']'&&buf[7]==' ')
+      {
+        char* nick=&buf[8];
+        char* msg=strchr(nick, ' ');
+        if(msg[-1]==':')
+        {
+          nick=strchr(nick, 'm')+1;
+          char* nickend=&msg[-1];
+          msg=&msg[1];
+          if(!strncmp(msg, "/msg ", 5)) // message is a PM
+          {
+            char* pm=strchr(&msg[5], ' ');
+            if(!pm){waddstr(buffers[0].pad, "\npm is null!"); continue;}
+            pm=&pm[1];
+            nickend[0]=0;
+            buffer=findbuffer(nick);
+            if(!buffer){buffer=createbuffer(nick);}
+            nickend[0]=':';
+            memmove(msg, pm, strlen(pm)+1);
+            if(buffer!=currentbuf)
+            {
+              buffers[buffer].seen=0;
+              drawtopic();
+            }
+          }
+        }
+        else if(!strncmp(msg, " changed nickname to ", 21))
+        {
+          msg[0]=0;
+          unsigned int i;
+          // Prevent duplicate names for buffers, and all the issues that would bring
+          if((i=findbuffer(&msg[21])))
+          {
+            renamebufferunique(i);
+          }
+          for(i=1; i<buffercount; ++i)
+          {
+            if(!strcmp(buffers[i].name, nick))
+            {
+              free(buffers[i].name);
+              buffers[i].name=strdup(&msg[21]);
+            }
+          }
+          msg[0]=' ';
+        }
       }
-      waddstr(chat, "\n");
-      waddansi(chat, buf);
+      waddstr(buffers[buffer].pad, "\n");
+      waddansi(buffers[buffer].pad, buf);
       drawchat();
       wrefresh(input);
       continue;
diff --git a/utilities/modbot/modbot.c b/utilities/modbot/modbot.c
index 1041544..e9d1111 100644
--- a/utilities/modbot/modbot.c
+++ b/utilities/modbot/modbot.c
@@ -547,7 +547,7 @@ int main(int argc, char** argv)
             unsigned int num=((msg[5]&&msg[6])?strtoul(&msg[6], 0, 0):1);
             if(num<1){say(pm, "The given value evaluates to 0, please specify the number of videos you would like to skip (or if you do not specify it will default to 1)\n"); continue;}
             if(playing){free(playing); playing=0; --num; say(0, "/mbc youTube\n");}
-            while(num>0)
+            while(num>0&&queue.itemcount>0)
             {
               queue_del(&queue, queue.items[0].video);
               --num;
@@ -571,7 +571,7 @@ int main(int argc, char** argv)
             alarm(getduration(playing)-pos);
             started=time(0)-pos;
           }
-          else if(!strcmp(msg, "/mbc youTube")) // Video cancelled
+          else if(!strcmp(msg, "/mbc youTube") && playing) // Video cancelled
           {
             list_del(&goodvids, playing); // manual /mbc is often used when !badvid should be used, so at least remove it from the list of approved videos
             list_save(&goodvids, "goodvids.txt");