root/src/apps/midiplayer/MidiPlayerWindow.cpp
/*
 * Copyright (c) 2004 Matthijs Hollemans
 * Copyright (c) 2008-2014 Haiku, Inc. All rights reserved.
 *
 * 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.
 */


#include "MidiPlayerWindow.h"

#include <Catalog.h>
#include <LayoutBuilder.h>
#include <Locale.h>
#include <MidiProducer.h>
#include <MidiRoster.h>
#include <SeparatorView.h>
#include <StorageKit.h>
#include <SpaceLayoutItem.h>

#include "MidiPlayerApp.h"
#include "ScopeView.h"
#include "SynthBridge.h"


#define _W(a) (a->Frame().Width())
#define _H(a) (a->Frame().Height())


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Main Window"


//      #pragma mark - MidiPlayerWindow


MidiPlayerWindow::MidiPlayerWindow()
        :
        BWindow(BRect(0, 0, 1, 1), B_TRANSLATE_SYSTEM_NAME("MidiPlayer"),
                B_TITLED_WINDOW, B_ASYNCHRONOUS_CONTROLS | B_NOT_RESIZABLE
                        | B_NOT_ZOOMABLE | B_AUTO_UPDATE_SIZE_LIMITS)
{
        fIsPlaying = false;
        fScopeEnabled = true;
        fReverbMode = B_REVERB_BALLROOM;
        fVolume = 75;
        fWindowX = -1;
        fWindowY = -1;
        fInputId = -1;
        fInstrumentLoaded = false;

        be_synth->SetSamplingRate(44100);

        fSynthBridge = new SynthBridge;
        //fSynthBridge->Register();

        CreateViews();
        LoadSettings();
        InitControls();
}


MidiPlayerWindow::~MidiPlayerWindow()
{
        StopSynth();

        //fSynthBridge->Unregister();
        fSynthBridge->Release();
}


bool
MidiPlayerWindow::QuitRequested()
{
        be_app->PostMessage(B_QUIT_REQUESTED);
        return true;
}


void
MidiPlayerWindow::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case MSG_PLAY_STOP:
                        OnPlayStop();
                        break;

                case MSG_SHOW_SCOPE:
                        OnShowScope();
                        break;

                case MSG_INPUT_CHANGED:
                        OnInputChanged(message);
                        break;

                case MSG_REVERB_NONE:
                        OnReverb(B_REVERB_NONE);
                        break;

                case MSG_REVERB_CLOSET:
                        OnReverb(B_REVERB_CLOSET);
                        break;

                case MSG_REVERB_GARAGE:
                        OnReverb(B_REVERB_GARAGE);
                        break;

                case MSG_REVERB_IGOR:
                        OnReverb(B_REVERB_BALLROOM);
                        break;

                case MSG_REVERB_CAVERN:
                        OnReverb(B_REVERB_CAVERN);
                        break;

                case MSG_REVERB_DUNGEON:
                        OnReverb(B_REVERB_DUNGEON);
                        break;

                case MSG_VOLUME:
                        OnVolume();
                        break;

                case B_SIMPLE_DATA:
                        OnDrop(message);
                        break;

                default:
                        super::MessageReceived(message);
                        break;
        }
}


void
MidiPlayerWindow::FrameMoved(BPoint origin)
{
        super::FrameMoved(origin);
        fWindowX = Frame().left;
        fWindowY = Frame().top;
        SaveSettings();
}


void
MidiPlayerWindow::MenusBeginning()
{
        for (int32 t = fInputPopUpMenu->CountItems() - 1; t > 0; --t)
                delete fInputPopUpMenu->RemoveItem(t);

        // Note: if the selected endpoint no longer exists, then no endpoint is
        // marked. However, we won't disconnect it until you choose another one.

        fInputOffMenuItem->SetMarked(fInputId == -1);

        int32 id = 0;
        while (BMidiEndpoint* endpoint = BMidiRoster::NextEndpoint(&id)) {
                if (endpoint->IsProducer()) {
                        BMessage* message = new BMessage(MSG_INPUT_CHANGED);
                        message->AddInt32("id", id);

                        BMenuItem* item = new BMenuItem(endpoint->Name(), message);
                        fInputPopUpMenu->AddItem(item);
                        item->SetMarked(fInputId == id);
                }

                endpoint->Release();
        }
}


void
MidiPlayerWindow::CreateInputMenu()
{
        fInputPopUpMenu = new BPopUpMenu("inputPopUp");

        BMessage* message = new BMessage(MSG_INPUT_CHANGED);
        message->AddInt32("id", -1);

        fInputOffMenuItem = new BMenuItem(B_TRANSLATE("Off"), message);
        fInputPopUpMenu->AddItem(fInputOffMenuItem);

        fInputMenuField = new BMenuField(B_TRANSLATE("Live input:"),
                fInputPopUpMenu);
}


void
MidiPlayerWindow::CreateReverbMenu()
{
        BPopUpMenu* reverbPopUpMenu = new BPopUpMenu("reverbPopUp");
        fReverbNoneMenuItem = new BMenuItem(B_TRANSLATE("None"),
                new BMessage(MSG_REVERB_NONE));
        fReverbClosetMenuItem = new BMenuItem(B_TRANSLATE("Closet"),
                new BMessage(MSG_REVERB_CLOSET));
        fReverbGarageMenuItem = new BMenuItem(B_TRANSLATE("Garage"),
                new BMessage(MSG_REVERB_GARAGE));
        fReverbIgorMenuItem = new BMenuItem(B_TRANSLATE("Igor's lab"),
                new BMessage(MSG_REVERB_IGOR));
        fReverbCavern = new BMenuItem(B_TRANSLATE("Cavern"),
                new BMessage(MSG_REVERB_CAVERN));
        fReverbDungeon = new BMenuItem(B_TRANSLATE("Dungeon"),
                new BMessage(MSG_REVERB_DUNGEON));

        reverbPopUpMenu->AddItem(fReverbNoneMenuItem);
        reverbPopUpMenu->AddItem(fReverbClosetMenuItem);
        reverbPopUpMenu->AddItem(fReverbGarageMenuItem);
        reverbPopUpMenu->AddItem(fReverbIgorMenuItem);
        reverbPopUpMenu->AddItem(fReverbCavern);
        reverbPopUpMenu->AddItem(fReverbDungeon);

        fReverbMenuField = new BMenuField(B_TRANSLATE("Reverb:"), reverbPopUpMenu);
}


void
MidiPlayerWindow::CreateViews()
{
        // Set up needed views
        fScopeView = new ScopeView();

        fShowScopeCheckBox = new BCheckBox("showScope", B_TRANSLATE("Scope"),
                new BMessage(MSG_SHOW_SCOPE));
        fShowScopeCheckBox->SetValue(B_CONTROL_ON);

        CreateInputMenu();
        CreateReverbMenu();

        fVolumeSlider = new BSlider("volumeSlider", NULL, NULL, 0, 100,
                B_HORIZONTAL);
        rgb_color color = ui_color(B_CONTROL_HIGHLIGHT_COLOR);
        fVolumeSlider->UseFillColor(true, &color);
        fVolumeSlider->SetModificationMessage(new BMessage(MSG_VOLUME));
        fVolumeSlider->SetExplicitMinSize(
                BSize(fScopeView->Frame().Width(), B_SIZE_UNSET));

        fPlayButton = new BButton("playButton", B_TRANSLATE("Play"),
                new BMessage(MSG_PLAY_STOP));
        fPlayButton->SetEnabled(false);

        BStringView* volumeLabel = new BStringView(NULL, B_TRANSLATE("Volume:"));
        volumeLabel->SetAlignment(B_ALIGN_LEFT);

        // Build the layout
        BLayoutBuilder::Group<>(this, B_VERTICAL, 0)
                .Add(fScopeView)
                .AddGroup(B_VERTICAL, 0)
                        .AddGrid(B_USE_DEFAULT_SPACING, B_USE_SMALL_SPACING)
                                .Add(fShowScopeCheckBox, 1, 0)

                                .Add(fReverbMenuField->CreateLabelLayoutItem(), 0, 1)
                                .AddGroup(B_HORIZONTAL, 0.0f, 1, 1)
                                        .Add(fReverbMenuField->CreateMenuBarLayoutItem())
                                        .AddGlue()
                                        .End()

                                .Add(fInputMenuField->CreateLabelLayoutItem(), 0, 2)
                                .AddGroup(B_HORIZONTAL, 0.0f, 1, 2)
                                        .Add(fInputMenuField->CreateMenuBarLayoutItem())
                                        .AddGlue()
                                        .End()

                                .Add(volumeLabel, 0, 3)
                                .Add(fVolumeSlider, 0, 4, 2, 1)
                                .End()
                        .AddGlue()
                        .SetInsets(B_USE_WINDOW_SPACING, B_USE_DEFAULT_SPACING,
                                B_USE_WINDOW_SPACING, B_USE_DEFAULT_SPACING)

                        .End()
                .Add(new BSeparatorView(B_HORIZONTAL))
                .AddGroup(B_VERTICAL, 0)
                        .Add(fPlayButton)
                        .SetInsets(0, B_USE_DEFAULT_SPACING, 0, B_USE_WINDOW_SPACING)
                        .End()
                .End();
}


void
MidiPlayerWindow::InitControls()
{
        Lock();

        fShowScopeCheckBox->SetValue(fScopeEnabled ? B_CONTROL_ON : B_CONTROL_OFF);
        fScopeView->SetEnabled(fScopeEnabled);

        fInputOffMenuItem->SetMarked(true);

        fReverbNoneMenuItem->SetMarked(fReverbMode == B_REVERB_NONE);
        fReverbClosetMenuItem->SetMarked(fReverbMode == B_REVERB_CLOSET);
        fReverbGarageMenuItem->SetMarked(fReverbMode == B_REVERB_GARAGE);
        fReverbIgorMenuItem->SetMarked(fReverbMode == B_REVERB_BALLROOM);
        fReverbCavern->SetMarked(fReverbMode == B_REVERB_CAVERN);
        fReverbDungeon->SetMarked(fReverbMode == B_REVERB_DUNGEON);
        be_synth->SetReverb(fReverbMode);

        fVolumeSlider->SetValue(fVolume);

        if (fWindowX != -1 && fWindowY != -1)
                MoveTo(fWindowX, fWindowY);
        else
                CenterOnScreen();

        Unlock();
}


void
MidiPlayerWindow::LoadSettings()
{
        BFile file(SETTINGS_FILE, B_READ_ONLY);
        if (file.InitCheck() != B_OK || file.Lock() != B_OK)
                return;

        file.ReadAttr("Scope", B_BOOL_TYPE, 0, &fScopeEnabled, sizeof(bool));
        file.ReadAttr("Reverb", B_INT32_TYPE, 0, &fReverbMode, sizeof(int32));
        file.ReadAttr("Volume", B_INT32_TYPE, 0, &fWindowX, sizeof(int32));
        file.ReadAttr("WindowX", B_FLOAT_TYPE, 0, &fWindowX, sizeof(float));
        file.ReadAttr("WindowY", B_FLOAT_TYPE, 0, &fWindowY, sizeof(float));

        file.Unlock();
}


void
MidiPlayerWindow::SaveSettings()
{
        BFile file(SETTINGS_FILE, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
        if (file.InitCheck() != B_OK || file.Lock() != B_OK)
                return;

        file.WriteAttr("Scope", B_BOOL_TYPE, 0, &fScopeEnabled, sizeof(bool));
        file.WriteAttr("Reverb", B_INT32_TYPE, 0, &fReverbMode, sizeof(int32));
        file.WriteAttr("Volume", B_INT32_TYPE, 0, &fVolume, sizeof(int32));
        file.WriteAttr("WindowX", B_FLOAT_TYPE, 0, &fWindowX, sizeof(float));
        file.WriteAttr("WindowY", B_FLOAT_TYPE, 0, &fWindowY, sizeof(float));

        file.Sync();
        file.Unlock();
}


void
MidiPlayerWindow::LoadFile(entry_ref* ref)
{
        if (fIsPlaying) {
                fScopeView->SetPlaying(false);
                fScopeView->Invalidate();
                UpdateIfNeeded();

                StopSynth();
        }

        fMidiSynthFile.UnloadFile();

        if (fMidiSynthFile.LoadFile(ref) == B_OK) {
                // Ideally, we would call SetVolume() in InitControls(),
                // but for some reason that doesn't work: BMidiSynthFile
                // will use the default volume instead. So we do it here.
                fMidiSynthFile.SetVolume(fVolume / 100.0f);

                fPlayButton->SetEnabled(true);
                fPlayButton->SetLabel(B_TRANSLATE("Stop"));
                fScopeView->SetHaveFile(true);
                fScopeView->SetPlaying(true);
                fScopeView->Invalidate();

                StartSynth();
        } else {
                fPlayButton->SetEnabled(false);
                fPlayButton->SetLabel(B_TRANSLATE("Play"));
                fScopeView->SetHaveFile(false);
                fScopeView->SetPlaying(false);
                fScopeView->Invalidate();

                BAlert* alert = new BAlert(NULL, B_TRANSLATE("Could not load song"),
                        B_TRANSLATE("OK"), NULL, NULL,
                        B_WIDTH_AS_USUAL, B_STOP_ALERT);
                alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
                alert->Go();
        }
}


void
MidiPlayerWindow::StartSynth()
{
        fMidiSynthFile.Start();
        fMidiSynthFile.SetFileHook(_StopHook, (int32)(addr_t)this);
        fIsPlaying = true;
}


void
MidiPlayerWindow::StopSynth()
{
        if (!fMidiSynthFile.IsFinished())
                fMidiSynthFile.Fade();

        fIsPlaying = false;
}


void
MidiPlayerWindow::_StopHook(int32 arg)
{
        ((MidiPlayerWindow*)(addr_t)arg)->StopHook();
}


void
MidiPlayerWindow::StopHook()
{
        if (Lock()) {
                // we may be called from the synth's thread

                fIsPlaying = false;

                fScopeView->SetPlaying(false);
                fScopeView->Invalidate();
                fPlayButton->SetEnabled(true);
                fPlayButton->SetLabel(B_TRANSLATE("Play"));

                Unlock();
        }
}


void
MidiPlayerWindow::OnPlayStop()
{
        if (fIsPlaying) {
                fPlayButton->SetEnabled(false);
                fScopeView->SetPlaying(false);
                fScopeView->Invalidate();
                UpdateIfNeeded();

                StopSynth();
        } else {
                fPlayButton->SetLabel(B_TRANSLATE("Stop"));
                fScopeView->SetPlaying(true);
                fScopeView->Invalidate();

                StartSynth();
        }
}


void
MidiPlayerWindow::OnShowScope()
{
        fScopeEnabled = !fScopeEnabled;
        fScopeView->SetEnabled(fScopeEnabled);
        fScopeView->Invalidate();
        SaveSettings();
}


void
MidiPlayerWindow::OnInputChanged(BMessage* message)
{
        int32 newId;
        if (message->FindInt32("id", &newId) == B_OK) {
                BMidiProducer* endpoint = BMidiRoster::FindProducer(fInputId);
                if (endpoint != NULL) {
                        endpoint->Disconnect(fSynthBridge);
                        endpoint->Release();
                }

                fInputId = newId;

                endpoint = BMidiRoster::FindProducer(fInputId);
                if (endpoint != NULL) {
                        if (!fInstrumentLoaded) {
                                fScopeView->SetLoading(true);
                                fScopeView->Invalidate();
                                UpdateIfNeeded();

                                fSynthBridge->Init(B_BIG_SYNTH);
                                fInstrumentLoaded = true;

                                fScopeView->SetLoading(false);
                                fScopeView->Invalidate();
                        }

                        endpoint->Connect(fSynthBridge);
                        endpoint->Release();

                        fScopeView->SetLiveInput(true);
                        fScopeView->Invalidate();
                } else {
                        fScopeView->SetLiveInput(false);
                        fScopeView->Invalidate();
                }
        }
}


void
MidiPlayerWindow::OnReverb(reverb_mode mode)
{
        fReverbMode = mode;
        be_synth->SetReverb(fReverbMode);
        SaveSettings();
}


void
MidiPlayerWindow::OnVolume()
{
        fVolume = fVolumeSlider->Value();
        fMidiSynthFile.SetVolume(fVolume / 100.0f);
        SaveSettings();
}


void
MidiPlayerWindow::OnDrop(BMessage* message)
{
        entry_ref ref;
        if (message->FindRef("refs", &ref) == B_OK)
                LoadFile(&ref);
}