drumstick  0.5.0
drumgrid.cpp

Simple drum patterns

/*
    MIDI Sequencer C++ library
    Copyright (C) 2006-2010, Pedro Lopez-Cabanillas <plcl@users.sf.net>

    This library is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This library 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 General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#ifndef DRUMGRID_H
#define DRUMGRID_H

#include "drumgridabout.h"

#include <QtGui/QMainWindow>
#include <QtGui/QShortcut>
#include <QtGui/QCloseEvent>
#include <QtCore/QSignalMapper>

const QString QSTR_WINDOW("Window");
const QString QSTR_GEOMETRY("Geometry");
const QString QSTR_STATE("State");
const QString QSTR_MIDI("MIDI");
const QString QSTR_CONNECTION("Connection");
const QString QSTR_TEMPO("Tempo");
const QString QSTR_PATTERN("Pattern");

const int TEMPO_MIN(25);
const int TEMPO_MAX(250);
const int TEMPO_DEFAULT(120);
const int NOTE_DURATION(10);
const int METRONOME_CHANNEL(9);
const int METRONOME_VELOCITY(100);
const int METRONOME_PROGRAM(0);
const int METRONOME_RESOLUTION(120);
const int METRONOME_VOLUME(100);
const int METRONOME_PAN(64);
const int VOLUME_CC(7);
const int PAN_CC(10);

namespace Ui
{
    class DrumGrid;
}

namespace drumstick
{
    class MidiClient;
    class MidiPort;
    class MidiQueue;
    class SequencerEvent;
}

class DrumGridModel;

using namespace drumstick;

class DrumGrid : public QMainWindow
{
    Q_OBJECT

public:
    DrumGrid(QWidget *parent = 0);
    ~DrumGrid();
    void subscribe(const QString& portName);
    void addShortcut(const QKeySequence& key, const QString& value);
    void readSettings();
    void writeSettings();
    void closeEvent( QCloseEvent *event );

    void metronome_start();
    void metronome_stop();
    void metronome_continue();

    void sendControlChange(int cc, int value);
    void sendInitialControls();
    void metronome_set_controls();
    void metronome_set_program();
    void metronome_set_tempo();

    void metronome_pattern(int tick);
    void metronome_echo(int tick, int ev_type);
    void metronome_note(int note, int vel, int tick);
    void metronome_schedule_event(SequencerEvent* ev, int tick);
    void metronome_event_output(SequencerEvent* ev);
    int decodeVelocity(const QString drumVel);

public slots:
    void slotAbout();
    void slotAboutQt();
    void updateView();
    void sequencerEvent(SequencerEvent *ev);
    void connectMidi();
    void play();
    void stop();
    void tempoChange(int newTempo);
    void gridColumns(int columns);
    void shortcutPressed(const QString& value);
    void updateDisplay(int bar, int beat);

signals:
    void signalUpdate(int bar, int beat);

private:
    Ui::DrumGrid *m_ui;
    int m_clientId;
    int m_portId;
    int m_queueId;
    unsigned long m_tick;
    MidiClient* m_Client;
    MidiPort* m_Port;
    MidiQueue* m_Queue;
    DrumGridModel* m_model;
    QString m_subscription;
    QSignalMapper* m_mapper;
    QVector<QShortcut*> m_shortcuts;
    About dlgAbout;

    int m_bar;
    int m_beat;
    int m_weak_velocity;
    int m_strong_velocity;
    int m_program;
    int m_channel;
    int m_volume;
    int m_pan;
    int m_resolution;
    int m_bpm;
    int m_noteDuration;
    int m_patternDuration;
    bool m_autoconnect;
    bool m_playing;
    bool m_useNoteOff;
};

#endif // DRUMGRID_H
/*
    MIDI Sequencer C++ library
    Copyright (C) 2006-2010, Pedro Lopez-Cabanillas <plcl@users.sf.net>

    This library is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This library 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 General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#include "drumgrid.h"
#include "drumgridmodel.h"
#include "ui_drumgrid.h"
#include "drumgridabout.h"

#include <QtGui/QInputDialog>
#include <QtGui/QShortcut>
#include <QtGui/QToolTip>
#include <QtCore/QSignalMapper>
#include <QtCore/QSettings>
#include <qmath.h>
#include "alsaclient.h"
#include "alsaport.h"
#include "alsaqueue.h"
#include "alsaevent.h"

DrumGrid::DrumGrid(QWidget *parent)
    : QMainWindow(parent),
    m_ui(new Ui::DrumGrid),
    m_clientId(-1),
    m_portId(-1),
    m_queueId(-1),
    m_tick(0),
    m_weak_velocity(METRONOME_VELOCITY / 2),
    m_strong_velocity(METRONOME_VELOCITY),
    m_program(METRONOME_PROGRAM),
    m_channel(METRONOME_CHANNEL),
    m_volume(METRONOME_VOLUME),
    m_pan(METRONOME_PAN),
    m_resolution(METRONOME_RESOLUTION),
    m_bpm(TEMPO_DEFAULT),
    m_noteDuration(NOTE_DURATION),
    m_autoconnect(false),
    m_playing(false),
    m_useNoteOff(true)
{
    m_ui->setupUi(this);
    m_ui->startButton->setIcon(style()->standardIcon(QStyle::StandardPixmap(QStyle::SP_MediaPlay)));
    m_ui->startButton->setShortcut(Qt::Key_MediaPlay);
    m_ui->stopButton->setIcon(style()->standardIcon(QStyle::StandardPixmap(QStyle::SP_MediaStop)));
    m_ui->stopButton->setShortcut(Qt::Key_MediaStop);
    m_ui->tempoSlider->setMaximum(TEMPO_MAX);
    m_ui->tempoSlider->setMinimum(TEMPO_MIN);
    m_ui->tempoSlider->setValue(m_bpm);

    connect( m_ui->actionAbout, SIGNAL(triggered()), SLOT(slotAbout()));
    connect( m_ui->actionAbout_Qt, SIGNAL(triggered()), SLOT(slotAboutQt()));
    connect( m_ui->actionQuit, SIGNAL(triggered()), SLOT(close()));
    connect( m_ui->actionConnect, SIGNAL(triggered()), SLOT(connectMidi()));
    connect( m_ui->startButton, SIGNAL(clicked()), SLOT(play()));
    connect( m_ui->stopButton, SIGNAL(clicked()), SLOT(stop()));
    connect( m_ui->tempoSlider, SIGNAL(valueChanged(int)), SLOT(tempoChange(int)));
    connect( m_ui->gridColumns, SIGNAL(valueChanged(int)), SLOT(gridColumns(int)));

    m_model = new DrumGridModel(this);
    m_model->fillSampleData();
    m_ui->tableView->setModel(m_model);
    connect ( this, SIGNAL(signalUpdate(int,int)), SLOT(updateDisplay(int,int)) );

    m_mapper = new QSignalMapper(this);
    addShortcut(QKeySequence("f"), "f");
    addShortcut(QKeySequence("p"), "p");
    addShortcut(QKeySequence("1"), "1");
    addShortcut(QKeySequence("2"), "2");
    addShortcut(QKeySequence("3"), "3");
    addShortcut(QKeySequence("4"), "4");
    addShortcut(QKeySequence("5"), "5");
    addShortcut(QKeySequence("6"), "6");
    addShortcut(QKeySequence("7"), "7");
    addShortcut(QKeySequence("8"), "8");
    addShortcut(QKeySequence("9"), "9");
    addShortcut(QKeySequence("0"), QString());
    addShortcut(QKeySequence::Delete, QString());
    connect( m_mapper, SIGNAL(mapped(QString)), SLOT(shortcutPressed(QString)));
    connect ( m_ui->tableView, SIGNAL(doubleClicked(const QModelIndex&)),
              m_model, SLOT(changeCell(const QModelIndex &)) );

    m_Client = new MidiClient(this);
    m_Client->open();
    m_Client->setClientName("DrumGrid");
    connect( m_Client, SIGNAL(eventReceived(SequencerEvent*)),
             SLOT(sequencerEvent(SequencerEvent*)), Qt::QueuedConnection );
    m_Port = new MidiPort(this);
    m_Port->attach( m_Client );
    m_Port->setPortName("DrumGrid Output Port");
    m_Port->setCapability( SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_WRITE |
                           SND_SEQ_PORT_CAP_SUBS_READ );
    m_Port->setPortType( SND_SEQ_PORT_TYPE_APPLICATION |
                         SND_SEQ_PORT_TYPE_MIDI_GENERIC );
    m_Queue = m_Client->createQueue("DrumGrid");
    m_queueId = m_Queue->getId();
    m_portId = m_Port->getPortId();
    m_clientId = m_Client->getClientId();
    m_Client->setRealTimeInput(false);
    m_Client->startSequencerInput();
    readSettings();
    updateView();
}

DrumGrid::~DrumGrid()
{
    foreach(QShortcut* s, m_shortcuts)
        delete s;
    m_Port->detach();
    m_Client->close();
    delete m_ui;
}

void DrumGrid::updateView()
{
    m_ui->tableView->resizeColumnsToContents();
    m_ui->tableView->resizeRowsToContents();
}

void DrumGrid::subscribe(const QString& portName)
{
    try {
        if (!m_subscription.isEmpty()) {
            m_Port->unsubscribeTo(m_subscription);
            m_subscription.clear();
        }
        m_Port->subscribeTo(portName);
        m_subscription = portName;
    } catch (const SequencerError& err) {
        qWarning() << "SequencerError exception. Error code: " << err.code()
                   << " (" << err.qstrError() << ")";
        qWarning() << "Location: " << err.location();
    }
}

void DrumGrid::connectMidi()
{
    bool ok;
    int current;
    QStringList items;
    QListIterator<PortInfo> it(m_Client->getAvailableOutputs());
    while(it.hasNext()) {
        PortInfo p = it.next();
        items << QString("%1:%2").arg(p.getClientName()).arg(p.getPort());
    }
    current = items.indexOf(m_subscription);
    QString item = QInputDialog::getItem(this, "MIDI port subscription",
                                         "Output port:", items,
                                         current, false, &ok);
    if (ok && !item.isEmpty())
        subscribe(item);
}

void DrumGrid::sequencerEvent(SequencerEvent *ev)
{
    switch (ev->getSequencerType()) {
    case SND_SEQ_EVENT_USR0:
        metronome_pattern(ev->getTick());
        m_bar++;
        m_beat = 0;
        break;
    case SND_SEQ_EVENT_USR1:
        m_beat++;
        emit signalUpdate(m_bar, m_beat-1);
        break;
    }
    delete ev;
}

void DrumGrid::play()
{
    metronome_set_tempo();
    metronome_start();
}

void DrumGrid::stop()
{
    metronome_stop();
}

void DrumGrid::tempoChange(int newTempo)
{
    QString tip = QString::number(newTempo);
    m_bpm = newTempo;
    metronome_set_tempo();
    m_ui->tempoSlider->setToolTip(tip);
    QToolTip::showText(QCursor::pos(), tip, this);
}

void DrumGrid::gridColumns(int columns)
{
    m_model->updatePatternColumns(columns);
    updateView();
}

void DrumGrid::shortcutPressed(const QString& value)
{
    QModelIndex index = m_ui->tableView->currentIndex();
    m_model->changeCell(index, value);
}

void DrumGrid::addShortcut(const QKeySequence& key, const QString& value)
{
    QShortcut* shortcut = new QShortcut(key, m_ui->tableView);
    connect (shortcut, SIGNAL(activated()), m_mapper, SLOT(map()));
    m_mapper->setMapping(shortcut, value);
    m_shortcuts.append(shortcut);
}

void DrumGrid::readSettings()
{
    QSettings settings;
    settings.beginGroup(QSTR_WINDOW);
    restoreGeometry(settings.value(QSTR_GEOMETRY).toByteArray());
    restoreState(settings.value(QSTR_STATE).toByteArray());
    settings.endGroup();

    settings.beginGroup(QSTR_MIDI);
    QString midiConn = settings.value(QSTR_CONNECTION).toString();
    m_bpm = settings.value(QSTR_TEMPO, TEMPO_DEFAULT).toInt();
    settings.endGroup();
    if (midiConn.length() > 0) {
        subscribe(midiConn);
    }

    settings.beginGroup(QSTR_PATTERN);
    QStringList keys = settings.allKeys();
    if (!keys.empty()) {
        keys.sort();
        m_model->clearPattern();
        foreach(const QString& key, keys) {
            QStringList row = settings.value(key).toStringList();
            m_model->addPatternData(key.toInt(), row);
        }
        m_model->endOfPattern();
    }
    settings.endGroup();
}

void DrumGrid::writeSettings()
{
    QSettings settings;
    settings.clear();

    settings.beginGroup(QSTR_WINDOW);
    settings.setValue(QSTR_GEOMETRY, saveGeometry());
    settings.setValue(QSTR_STATE, saveState());
    settings.endGroup();

    settings.beginGroup(QSTR_MIDI);
    settings.setValue(QSTR_CONNECTION, m_subscription);
    settings.setValue(QSTR_TEMPO, m_bpm);
    settings.endGroup();

    settings.beginGroup(QSTR_PATTERN);
    for(int r = 0; r < m_model->rowCount(); ++r) {
        settings.setValue( m_model->patternKey(r),
                           m_model->patternData(r) );
    }
    settings.endGroup();

    settings.sync();
}

void DrumGrid::closeEvent( QCloseEvent *event )
{
    writeSettings();
    event->accept();
}

void DrumGrid::metronome_event_output(SequencerEvent* ev)
{
    ev->setSource(m_portId);
    ev->setSubscribers();
    ev->setDirect();
    m_Client->outputDirect(ev);
}

void DrumGrid::sendControlChange(int cc, int value)
{
    ControllerEvent ev(m_channel, cc, value);
    metronome_event_output(&ev);
}

void DrumGrid::sendInitialControls()
{
    metronome_set_program();
    metronome_set_controls();
    metronome_set_tempo();
}

void DrumGrid::metronome_set_program()
{
    ProgramChangeEvent ev(m_channel, m_program);
    metronome_event_output(&ev);
}

void DrumGrid::metronome_schedule_event(SequencerEvent* ev, int tick)
{
    ev->setSource(m_portId);
    if (ev->getSequencerType() >= SND_SEQ_EVENT_USR0)
        ev->setDestination(m_clientId, m_portId);
    else
        ev->setSubscribers();
    ev->scheduleTick(m_queueId, tick, false);
    m_Client->outputDirect(ev);
}

void DrumGrid::metronome_note(int note, int vel, int tick)
{
    if (m_useNoteOff) {
        NoteEvent ev(m_channel, note, vel, m_noteDuration);
        metronome_schedule_event(&ev, tick);
    } else {
        NoteOnEvent ev(m_channel, note, vel);
        metronome_schedule_event(&ev, tick);
    }
}

void DrumGrid::metronome_echo(int tick, int ev_type)
{
    SystemEvent ev(ev_type);
    metronome_schedule_event(&ev, tick);
}

int DrumGrid::decodeVelocity(const QString drumVel)
{
    const qreal f = 127.0 / 9.0;
    int num = 0;
    bool isNum = false;
    if (drumVel.isEmpty())
        return 0;
    if (drumVel == "f")
        return m_strong_velocity;
    else if (drumVel == "p")
        return m_weak_velocity;
    num = drumVel.toInt(&isNum);
    if (isNum)
        return qRound(f * num);
    return 0;
}

void DrumGrid::metronome_pattern(int tick)
{
    int i, j, t, duration, key, vel;
    t = tick;
    duration = m_resolution / 4;
    for(i=0; i<m_model->columnCount(); ++i) {
        for(j=0; j<m_model->rowCount(); ++j) {
            QString n = m_model->patternHit(j, i);
            if (!n.isEmpty()) {
                key = m_model->patternKey(j).toInt();
                vel = decodeVelocity(n);
                metronome_note(key, vel, t);
            }
        }
        metronome_echo(t, SND_SEQ_EVENT_USR1);
        t += duration;
    }
    metronome_echo(t, SND_SEQ_EVENT_USR0);
}

void DrumGrid::metronome_set_tempo()
{
    QueueTempo t = m_Queue->getTempo();
    t.setPPQ(m_resolution);
    t.setNominalBPM(m_bpm);
    m_Queue->setTempo(t);
    m_Client->drainOutput();
}

void DrumGrid::metronome_set_controls()
{
    sendControlChange(VOLUME_CC, m_volume);
    sendControlChange(PAN_CC, m_pan);
}

void DrumGrid::metronome_start()
{
    m_Queue->start();
    m_patternDuration = m_resolution * m_model->columnCount() / 4;
    metronome_pattern(0);
    m_bar = 1;
    m_beat = 0;
    m_playing = true;
}

void DrumGrid::metronome_stop()
{
    m_Queue->stop();
    m_playing = false;
}

void DrumGrid::updateDisplay(int /*bar*/, int beat)
{
    m_ui->tableView->selectColumn(beat);
}

void DrumGrid::slotAbout()
{
    dlgAbout.exec();
}

void DrumGrid::slotAboutQt()
{
    qApp->aboutQt();
}