diff --git a/Projector.sln b/Projector.sln
new file mode 100644
index 0000000..0a9b12a
--- /dev/null
+++ b/Projector.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.36202.13 d17.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Projector", "Projector\Projector.csproj", "{B61419F5-CBDA-4DEF-B7A9-30DD5BCC467F}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B61419F5-CBDA-4DEF-B7A9-30DD5BCC467F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B61419F5-CBDA-4DEF-B7A9-30DD5BCC467F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B61419F5-CBDA-4DEF-B7A9-30DD5BCC467F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B61419F5-CBDA-4DEF-B7A9-30DD5BCC467F}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {61C9AAF3-56EB-47A0-AB37-C40A3F4851E9}
+ EndGlobalSection
+EndGlobal
diff --git a/Projector/App.config b/Projector/App.config
new file mode 100644
index 0000000..ac55d9e
--- /dev/null
+++ b/Projector/App.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Projector/FourChannelToStereoSampleProvider.cs b/Projector/FourChannelToStereoSampleProvider.cs
new file mode 100644
index 0000000..6b87360
--- /dev/null
+++ b/Projector/FourChannelToStereoSampleProvider.cs
@@ -0,0 +1,84 @@
+using NAudio.Wave;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Projector
+{
+ // Custom ISampleProvider to mix 4 channels to stereo
+ public class FourChannelToStereoSampleProvider : ISampleProvider
+ {
+ private readonly ISampleProvider _source;
+ private float offTransition = 1f;
+ private float _voiceVolume = 0f;
+ private bool goingOff = false;
+
+ public WaveFormat WaveFormat { get; }
+
+ public void SetOff()
+ {
+ goingOff = true;
+ }
+
+ public bool IsOff => goingOff && offTransition == 0f;
+
+ public float VoiceVolume
+ {
+ get
+ {
+ return _voiceVolume;
+ }
+ set
+ {
+ _voiceVolume = Math.Clamp(value, 0f, 1f);
+ }
+ }
+
+ public FourChannelToStereoSampleProvider(ISampleProvider source)
+ {
+ if (source.WaveFormat.Channels != 4)
+ throw new ArgumentException("Source must have 4 channels");
+ if (source.WaveFormat.Encoding != WaveFormatEncoding.IeeeFloat)
+ throw new ArgumentException("Source must be IEEE float format");
+
+ _source = source;
+ WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(source.WaveFormat.SampleRate, 2);
+ }
+
+ public int Read(float[] buffer, int offset, int count)
+ {
+ int samplesNeeded = (count / 2) * 4;
+ float[] sourceBuffer = new float[samplesNeeded];
+ int samplesRead = _source.Read(sourceBuffer, 0, samplesNeeded);
+
+ int stereoSamples = samplesRead / 4 * 2;
+ int outIndex = offset;
+
+ if (goingOff && offTransition > 0f)
+ {
+ offTransition -= 0.2f;
+ if (offTransition <= 0f)
+ {
+ offTransition = 0f;
+ }
+ }
+
+ float musicVolume = 1f - VoiceVolume;
+ float voiceVolume = VoiceVolume;
+
+ for (int i = 0; i < samplesRead; i += 4)
+ {
+ // Mix channel 0 and 2 to left, 1 and 3 to right
+ float left = (sourceBuffer[i] * musicVolume) + (sourceBuffer[i + 2] * voiceVolume);
+ float right = (sourceBuffer[i + 1] * musicVolume) + (sourceBuffer[i + 3] * voiceVolume);
+ buffer[outIndex++] = left * offTransition;
+ buffer[outIndex++] = right * offTransition;
+ }
+
+ return stereoSamples;
+ }
+ }
+
+}
diff --git a/Projector/FrmMain.Designer.cs b/Projector/FrmMain.Designer.cs
new file mode 100644
index 0000000..39ad0c5
--- /dev/null
+++ b/Projector/FrmMain.Designer.cs
@@ -0,0 +1,109 @@
+namespace Projector
+{
+ partial class FrmMain
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ DgvHymns = new DataGridView();
+ CnHymn = new DataGridViewTextBoxColumn();
+ CnPlay = new DataGridViewButtonColumn();
+ TbFilter = new TextBox();
+ ((System.ComponentModel.ISupportInitialize)DgvHymns).BeginInit();
+ SuspendLayout();
+ //
+ // DgvHymns
+ //
+ DgvHymns.AllowUserToAddRows = false;
+ DgvHymns.AllowUserToDeleteRows = false;
+ DgvHymns.AllowUserToResizeColumns = false;
+ DgvHymns.AllowUserToResizeRows = false;
+ DgvHymns.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
+ DgvHymns.Columns.AddRange(new DataGridViewColumn[] { CnHymn, CnPlay });
+ DgvHymns.Dock = DockStyle.Fill;
+ DgvHymns.Location = new Point(0, 38);
+ DgvHymns.Margin = new Padding(5);
+ DgvHymns.MultiSelect = false;
+ DgvHymns.Name = "DgvHymns";
+ DgvHymns.ReadOnly = true;
+ DgvHymns.RowHeadersVisible = false;
+ DgvHymns.RowHeadersWidth = 51;
+ DgvHymns.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
+ DgvHymns.Size = new Size(665, 547);
+ DgvHymns.TabIndex = 7;
+ DgvHymns.CellPainting += DgvHymns_CellPainting;
+ //
+ // CnHymn
+ //
+ CnHymn.HeaderText = "Hymn";
+ CnHymn.MinimumWidth = 6;
+ CnHymn.Name = "CnHymn";
+ CnHymn.ReadOnly = true;
+ CnHymn.Width = 600;
+ //
+ // CnPlay
+ //
+ CnPlay.HeaderText = "P";
+ CnPlay.MinimumWidth = 6;
+ CnPlay.Name = "CnPlay";
+ CnPlay.ReadOnly = true;
+ CnPlay.Width = 32;
+ //
+ // TbFilter
+ //
+ TbFilter.BorderStyle = BorderStyle.FixedSingle;
+ TbFilter.Dock = DockStyle.Top;
+ TbFilter.Location = new Point(0, 0);
+ TbFilter.Margin = new Padding(5);
+ TbFilter.Name = "TbFilter";
+ TbFilter.Size = new Size(665, 38);
+ TbFilter.TabIndex = 6;
+ TbFilter.TextChanged += TbFilter_TextChanged;
+ //
+ // FrmMain
+ //
+ AutoScaleDimensions = new SizeF(13F, 31F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(665, 585);
+ Controls.Add(DgvHymns);
+ Controls.Add(TbFilter);
+ Font = new Font("Segoe UI", 13.8F, FontStyle.Regular, GraphicsUnit.Point, 0);
+ Margin = new Padding(5);
+ Name = "FrmMain";
+ Text = "SDA Church Projector";
+ Load += FrmMain_Load;
+ ((System.ComponentModel.ISupportInitialize)DgvHymns).EndInit();
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+ private TextBox TbFilter;
+ private DataGridView DgvHymns;
+ private DataGridViewTextBoxColumn CnHymn;
+ private DataGridViewButtonColumn CnPlay;
+ }
+}
diff --git a/Projector/FrmMain.cs b/Projector/FrmMain.cs
new file mode 100644
index 0000000..e180578
--- /dev/null
+++ b/Projector/FrmMain.cs
@@ -0,0 +1,288 @@
+using NAudio.Wave;
+using System;
+using System.Windows.Forms;
+using YoutubeExplode;
+using YoutubeExplode.Videos;
+using YoutubeExplode.Videos.Streams;
+
+namespace Projector
+{
+ public partial class FrmMain : Form
+ {
+ private Video? _video = null;
+ private StreamManifest? _streamManifest = null;
+ FourChannelToStereoSampleProvider? mixer = null;
+ string musicPath = "C:\\Hymnal\\Music";
+ string editsPath = "C:\\Hymnal\\Edits";
+ string LyricsPath = "C:\\Hymnal\\Lyrics";
+ FrmPreview frmPreview = new FrmPreview();
+
+ public FrmMain()
+ {
+ InitializeComponent();
+
+ DgvHymns.CellContentClick += DgvHymns_CellContentClick;
+ }
+
+ private void FrmMain_Load(object sender, EventArgs e)
+ {
+ FrmMain_Resize(null, null);
+
+ if (!System.IO.Directory.Exists(musicPath))
+ {
+ MessageBox.Show("Hymns directory does not exist: " + musicPath);
+ return;
+ }
+
+ var hymns = System.IO.Directory.GetFiles(musicPath, "*.m4a");
+ foreach (var hymn in hymns)
+ {
+ var fileName = System.IO.Path.GetFileNameWithoutExtension(hymn);
+ DgvHymns.Rows.Add(fileName, "");
+ }
+ }
+
+ private void DgvHymns_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
+ {
+ if (DgvHymns.Columns[e.ColumnIndex].Name == "CnPlay" && e.RowIndex >= 0)
+ {
+ e.Paint(e.CellBounds, DataGridViewPaintParts.All);
+ var icon = Properties.Resources.Play; // Assuming you have a play icon in resources
+ var iconRect = new Rectangle(e.CellBounds.X + 2, e.CellBounds.Y + 2, e.CellBounds.Height - 4, e.CellBounds.Height - 4);
+ e.Graphics?.DrawImage(icon, iconRect);
+ e.Handled = true; // Prevent default painting
+ }
+ }
+
+ // 3. Handle the Play button click:
+ private void DgvHymns_CellContentClick(object sender, DataGridViewCellEventArgs e)
+ {
+ // Check if PlayButton column and not header row
+ if (e.RowIndex >= 0 && DgvHymns.Columns[e.ColumnIndex].Name == "CnPlay")
+ {
+ var hymnName = DgvHymns.Rows[e.RowIndex].Cells["CnHymn"].Value?.ToString();
+ if (!string.IsNullOrEmpty(hymnName))
+ {
+ PlayHymn(hymnName);
+ }
+ }
+ }
+
+ private void PlayHymn(string name)
+ {
+ string audioFilePath = Path.Combine(editsPath, name + ".m4a");
+ if (!File.Exists(audioFilePath))
+ {
+ audioFilePath = Path.Combine(musicPath, name + ".m4a");
+ }
+
+ string number = name.Substring(0, 3).Trim(); // Extract the first 3 characters
+
+ try
+ {
+ // Open the audio file
+ var reader = new MediaFoundationReader(audioFilePath);
+
+ // Check for 4 channels
+ if (reader.WaveFormat.Channels != 4)
+ {
+ MessageBox.Show("Audio file does not have 4 channels.");
+ reader.Dispose();
+ return;
+ }
+
+ // Create a custom sample provider to mix channels 0+2 to left, 1+3 to right
+ mixer = new FourChannelToStereoSampleProvider(reader.ToSampleProvider());
+
+ // Search the folder in LyricsPath that starts with the hymn number
+ string lyricsFilePath = Directory.GetDirectories(LyricsPath).ToList().ConvertAll(d => Path.GetFileName(d))
+ .FirstOrDefault(dir => dir.StartsWith(number, StringComparison.OrdinalIgnoreCase));
+
+ // Get all the jpg files in the lyrics folder
+ if (lyricsFilePath != null)
+ {
+ string lyricsFolderPath = Path.Combine(LyricsPath, lyricsFilePath);
+ var jpgFiles = Directory.GetFiles(lyricsFolderPath, "*.jpg");
+
+ // Order the jpg files by name
+ Array.Sort(jpgFiles, StringComparer.OrdinalIgnoreCase);
+
+ // Display the first jpg file in a PictureBox or similar control
+ if (jpgFiles.Length > 0)
+ {
+ // Assuming you have a PictureBox named pictureBoxLyrics
+ frmPreview.SetImages(jpgFiles, mixer);
+ }
+ }
+
+ var waveOut = new WaveOutEvent();
+ waveOut.Init(mixer);
+ waveOut.Play();
+ frmPreview.ShowDialog(this);
+ TbFilter.SelectAll();
+ TbFilter.Focus();
+
+ // Crate a task to handle the transition
+ Task.Run(() =>
+ {
+ mixer.SetOff();
+ while (!mixer.IsOff)
+ {
+ Thread.Sleep(100); // Check every 100ms
+ }
+ waveOut.Stop();
+ });
+
+ // Optionally, store waveOut and reader as fields to stop/dispose later
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show("Error playing audio: " + ex.Message);
+ }
+ }
+
+ private void TbFilter_TextChanged(object sender, EventArgs e)
+ {
+ // Filter the DataGridView based on the text in the filter TextBox
+ string filterText = TbFilter.Text.ToLower();
+ foreach (DataGridViewRow row in DgvHymns.Rows)
+ {
+ if (row.Cells["CnHymn"].Value != null)
+ {
+ row.Visible = row.Cells["CnHymn"].Value.ToString().ToLower().Contains(filterText);
+ }
+ }
+
+ var visibleRows = DgvHymns.Rows
+ .Cast()
+ .Where(row => row.Visible)
+ .ToList();
+ if (visibleRows.Count > 0)
+ {
+ int firstVisibleRowIndex = visibleRows[0].Index;
+ DgvHymns.Rows[firstVisibleRowIndex].Selected = true;
+ DgvHymns.FirstDisplayedScrollingRowIndex = firstVisibleRowIndex;
+ }
+ }
+
+ private void FrmMain_Resize(object sender, EventArgs e)
+ {
+ DgvHymns.Columns["CnHymn"].Width = Width - 80;
+ }
+
+ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
+ {
+ if (keyData == Keys.Down)
+ {
+ var visibleRows = DgvHymns.Rows
+ .Cast()
+ .Where(row => row.Visible)
+ .ToList();
+
+ if (visibleRows.Count > 0)
+ {
+ int selectedRowIndex = visibleRows.FindIndex(row => row.Selected);
+ selectedRowIndex++;
+ if (selectedRowIndex < visibleRows.Count)
+ {
+ visibleRows[selectedRowIndex].Selected = true;
+
+ int firstDisplayed = DgvHymns.FirstDisplayedScrollingRowIndex;
+ int displayedCount = DgvHymns.DisplayedRowCount(false); // false = only fully visible rows
+ int lastDisplayed = firstDisplayed + displayedCount - 1;
+ if (selectedRowIndex >= lastDisplayed)
+ {
+ DgvHymns.FirstDisplayedScrollingRowIndex = visibleRows[selectedRowIndex - displayedCount + 1].Index;
+ }
+ }
+ }
+
+ return true;
+ }
+ else if (keyData == Keys.Up)
+ {
+ var visibleRows = DgvHymns.Rows
+ .Cast()
+ .Where(row => row.Visible)
+ .ToList();
+
+ if (visibleRows.Count > 0)
+ {
+ int selectedRowIndex = visibleRows.FindIndex(row => row.Selected);
+ selectedRowIndex--;
+ if (selectedRowIndex >= 0)
+ {
+ visibleRows[selectedRowIndex].Selected = true;
+
+ int firstDisplayed = DgvHymns.FirstDisplayedScrollingRowIndex;
+ int displayedCount = DgvHymns.DisplayedRowCount(false); // false = only fully visible rows
+
+ if (selectedRowIndex < firstDisplayed)
+ {
+ DgvHymns.FirstDisplayedScrollingRowIndex = visibleRows[selectedRowIndex].Index;
+ }
+ }
+ }
+
+ return true;
+ }
+ else if (keyData == Keys.Enter)
+ {
+ // Handle Enter key to simulate Play button click
+ var selectedRow = DgvHymns.SelectedRows.Cast().FirstOrDefault();
+ if (selectedRow != null && selectedRow.Cells["CnPlay"].Value != null)
+ {
+ DgvHymns_CellContentClick(sender: null, e: new DataGridViewCellEventArgs(1, selectedRow.Index));
+ return true; // Indicate that the key press has been handled
+ }
+ }
+ else if (keyData == Keys.Home)
+ {
+ var visibleRows = DgvHymns.Rows
+ .Cast()
+ .Where(row => row.Visible)
+ .ToList();
+
+ if (visibleRows.Count > 0)
+ {
+ visibleRows.First().Selected = true;
+ int selectedRowIndex = visibleRows.First().Index;
+
+ int firstDisplayed = DgvHymns.FirstDisplayedScrollingRowIndex;
+ int displayedCount = DgvHymns.DisplayedRowCount(false); // false = only fully visible rows
+
+ if (selectedRowIndex < firstDisplayed)
+ {
+ DgvHymns.FirstDisplayedScrollingRowIndex = visibleRows[selectedRowIndex].Index;
+ }
+ }
+ return true; // Indicate that the key press has been handled
+ }
+ else if (keyData == Keys.End)
+ {
+ var visibleRows = DgvHymns.Rows
+ .Cast()
+ .Where(row => row.Visible)
+ .ToList();
+
+ if (visibleRows.Count > 0)
+ {
+ visibleRows.Last().Selected = true;
+ int selectedRowIndex = visibleRows.Last().Index;
+
+ int firstDisplayed = DgvHymns.FirstDisplayedScrollingRowIndex;
+ int displayedCount = DgvHymns.DisplayedRowCount(false); // false = only fully visible rows
+ int lastDisplayed = firstDisplayed + displayedCount - 1;
+ if (selectedRowIndex >= lastDisplayed)
+ {
+ DgvHymns.FirstDisplayedScrollingRowIndex = visibleRows[selectedRowIndex - displayedCount + 1].Index;
+ }
+ }
+
+ return true; // Indicate that the key press has been handled
+ }
+
+ return base.ProcessCmdKey(ref msg, keyData); // Allow default processing if not an arrow key
+ }
+ }
+}
diff --git a/Projector/FrmMain.resx b/Projector/FrmMain.resx
new file mode 100644
index 0000000..87a0343
--- /dev/null
+++ b/Projector/FrmMain.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ True
+
+
+ True
+
+
\ No newline at end of file
diff --git a/Projector/FrmPreview.Designer.cs b/Projector/FrmPreview.Designer.cs
new file mode 100644
index 0000000..646071c
--- /dev/null
+++ b/Projector/FrmPreview.Designer.cs
@@ -0,0 +1,133 @@
+namespace Projector
+{
+ partial class FrmPreview
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmPreview));
+ PbShow = new PictureBox();
+ toolStrip1 = new ToolStrip();
+ TsbPrevious = new ToolStripButton();
+ TsbNext = new ToolStripButton();
+ TkVoice = new TrackBar();
+ ((System.ComponentModel.ISupportInitialize)PbShow).BeginInit();
+ toolStrip1.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)TkVoice).BeginInit();
+ SuspendLayout();
+ //
+ // PbShow
+ //
+ PbShow.BackColor = Color.Black;
+ PbShow.BorderStyle = BorderStyle.FixedSingle;
+ PbShow.Dock = DockStyle.Fill;
+ PbShow.Location = new Point(0, 0);
+ PbShow.Name = "PbShow";
+ PbShow.Size = new Size(550, 345);
+ PbShow.SizeMode = PictureBoxSizeMode.StretchImage;
+ PbShow.TabIndex = 0;
+ PbShow.TabStop = false;
+ //
+ // toolStrip1
+ //
+ toolStrip1.AutoSize = false;
+ toolStrip1.BackColor = Color.Black;
+ toolStrip1.Dock = DockStyle.Bottom;
+ toolStrip1.GripStyle = ToolStripGripStyle.Hidden;
+ toolStrip1.ImageScalingSize = new Size(220, 120);
+ toolStrip1.Items.AddRange(new ToolStripItem[] { TsbPrevious, TsbNext });
+ toolStrip1.Location = new Point(0, 345);
+ toolStrip1.Name = "toolStrip1";
+ toolStrip1.Size = new Size(599, 134);
+ toolStrip1.TabIndex = 1;
+ toolStrip1.Text = "toolStrip1";
+ //
+ // TsbPrevious
+ //
+ TsbPrevious.AutoSize = false;
+ TsbPrevious.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ TsbPrevious.Image = (Image)resources.GetObject("TsbPrevious.Image");
+ TsbPrevious.ImageTransparentColor = Color.Magenta;
+ TsbPrevious.Name = "TsbPrevious";
+ TsbPrevious.Size = new Size(236, 128);
+ TsbPrevious.Text = "Previous";
+ TsbPrevious.Click += TsbPrevious_Click;
+ //
+ // TsbNext
+ //
+ TsbNext.Alignment = ToolStripItemAlignment.Right;
+ TsbNext.AutoSize = false;
+ TsbNext.DisplayStyle = ToolStripItemDisplayStyle.Image;
+ TsbNext.Image = (Image)resources.GetObject("TsbNext.Image");
+ TsbNext.ImageTransparentColor = Color.Magenta;
+ TsbNext.Name = "TsbNext";
+ TsbNext.Size = new Size(236, 128);
+ TsbNext.Text = "Next";
+ TsbNext.Click += TsbNext_Click;
+ //
+ // TkVoice
+ //
+ TkVoice.AutoSize = false;
+ TkVoice.BackColor = Color.Black;
+ TkVoice.Dock = DockStyle.Right;
+ TkVoice.Location = new Point(550, 0);
+ TkVoice.Name = "TkVoice";
+ TkVoice.Orientation = Orientation.Vertical;
+ TkVoice.Size = new Size(49, 345);
+ TkVoice.TabIndex = 2;
+ TkVoice.TickStyle = TickStyle.Both;
+ TkVoice.Scroll += TkVoice_Scroll;
+ //
+ // FrmPreview
+ //
+ AutoScaleDimensions = new SizeF(8F, 20F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(599, 479);
+ Controls.Add(PbShow);
+ Controls.Add(TkVoice);
+ Controls.Add(toolStrip1);
+ FormBorderStyle = FormBorderStyle.SizableToolWindow;
+ Name = "FrmPreview";
+ StartPosition = FormStartPosition.CenterParent;
+ Text = "Preview";
+ FormClosed += FrmPreview_FormClosed;
+ Load += FrmPreview_Load;
+ ((System.ComponentModel.ISupportInitialize)PbShow).EndInit();
+ toolStrip1.ResumeLayout(false);
+ toolStrip1.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)TkVoice).EndInit();
+ ResumeLayout(false);
+ }
+
+ #endregion
+
+ private PictureBox PbShow;
+ private ToolStrip toolStrip1;
+ private ToolStripButton TsbPrevious;
+ private ToolStripButton TsbNext;
+ private TrackBar TkVoice;
+ }
+}
\ No newline at end of file
diff --git a/Projector/FrmPreview.cs b/Projector/FrmPreview.cs
new file mode 100644
index 0000000..2170cdb
--- /dev/null
+++ b/Projector/FrmPreview.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Projector
+{
+ public partial class FrmPreview : Form
+ {
+ private List imagesList = new List();
+ private int index = 0;
+ private FourChannelToStereoSampleProvider? _mixer = null;
+ private FrmProjector frmProjector = new FrmProjector();
+ private bool secondScreenEnabled = false;
+
+ public FrmPreview()
+ {
+ InitializeComponent();
+
+ Screen[] screens = Screen.AllScreens;
+ if (screens.Length > 1)
+ {
+ secondScreenEnabled = true;
+ Screen targetScreen = screens[1];
+ frmProjector.StartPosition = FormStartPosition.Manual;
+ frmProjector.Location = targetScreen.Bounds.Location;
+ }
+ }
+
+ public void SetImages(string[] images, FourChannelToStereoSampleProvider mixer)
+ {
+ if (secondScreenEnabled && !frmProjector.Visible)
+ {
+ frmProjector.Show();
+ }
+
+ _mixer = mixer;
+ mixer.VoiceVolume = TkVoice.Value / 10f;
+ imagesList.Clear();
+ imagesList.AddRange(images);
+ index = 0;
+ ShowImage(index);
+ }
+
+ public void Clear()
+ {
+ imagesList.Clear();
+ PbShow.ImageLocation = null;
+ }
+
+ private void ShowImage(int index)
+ {
+ if (index >= 0 && index < imagesList.Count)
+ {
+ frmProjector.SetImage(imagesList[index]);
+ PbShow.ImageLocation = imagesList[index];
+ this.Text = $"Preview - {System.IO.Path.GetFileName(imagesList[index])}";
+
+ if (index + 1 < imagesList.Count)
+ {
+ TsbNext.Image = Image.FromFile(imagesList[index + 1]);
+ }
+ else
+ {
+ TsbNext.Image = null; // No next image
+ }
+
+ if (index > 0)
+ {
+ TsbPrevious.Image = Image.FromFile(imagesList[index - 1]);
+ }
+ else
+ {
+ TsbPrevious.Image = null; // No previous image
+ }
+ }
+ else if (index == imagesList.Count)
+ {
+ frmProjector.SetImage(null);
+ PbShow.ImageLocation = null;
+ this.Text = $"Preview - End";
+ TsbNext.Image = null;
+ TsbPrevious.Image = Image.FromFile(imagesList[index - 1]);
+ }
+ else
+ {
+ frmProjector.SetImage(null);
+ PbShow.ImageLocation = null;
+ TsbNext.Image = null;
+ TsbPrevious.Image = null;
+ }
+ }
+
+ private void TsbPrevious_Click(object sender, EventArgs e)
+ {
+ if (index > 0)
+ {
+ index--;
+ ShowImage(index);
+ }
+ }
+
+ private void TsbNext_Click(object sender, EventArgs e)
+ {
+ if (index < imagesList.Count)
+ {
+ index++;
+ ShowImage(index);
+ }
+ }
+
+ private void FrmPreview_Load(object sender, EventArgs e)
+ {
+
+ }
+
+ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
+ {
+ if (keyData == Keys.Left)
+ {
+ // Handle Left arrow key press
+ TsbPrevious_Click(sender: null, e: null); // Simulate click on Previous button
+ return true; // Indicate that the key press has been handled and prevent default behavior
+ }
+ else if (keyData == Keys.Right)
+ {
+ // Handle Right arrow key press
+ TsbNext_Click(sender: null, e: null); // Simulate click on Next button
+ return true;
+ }
+ else if (keyData == Keys.Escape)
+ {
+ // Close the form when Escape is pressed
+ this.Close();
+ }
+
+ return base.ProcessCmdKey(ref msg, keyData); // Allow default processing if not an arrow key
+ }
+
+ private void TkVoice_Scroll(object sender, EventArgs e)
+ {
+ _mixer.VoiceVolume = TkVoice.Value / 10f;
+ }
+
+ private void FrmPreview_FormClosed(object sender, FormClosedEventArgs e)
+ {
+ frmProjector.SetImage(null);
+ //frmProjector.Close();
+ }
+ }
+}
diff --git a/Projector/FrmPreview.resx b/Projector/FrmPreview.resx
new file mode 100644
index 0000000..b65107f
--- /dev/null
+++ b/Projector/FrmPreview.resx
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 17, 17
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
+ YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAEESURBVEhL3ZKvDoJQFId5Dk0238GGr2DXbqBa3Cw6uy+g
+ RZozaTM4gxvZQHA6kc05/2CAetzP7bIrF0Hk3iLbx91O+L5xLloQBKQSDS/TNMkwDKnAGQYw0HVdKnAK
+ Ae/+kIK0wPV0JHvWI3veo4M1kR+wBjot24UQFpEW4OUAX6I2MJMc2K2GoRzrwp18HXDOHjUXp9cZFaeR
+ GoC0OnWpODq8zqyRxMDGvYdyRtbIx8B6f6Py2HmT/xKJDVRqjY/ytAhm/FwI1FtdKg23gjCOaITdFz8X
+ An3rIoiSYDL+Z2BzrDl3gMmiPwPAmrHu3IEksG6sXVkAwPlHAZVovu93VKKpfp4ISreGcqlKAwAAAABJ
+ RU5ErkJggg==
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
+ YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAEESURBVEhL3ZKvDoJQFId5Dk0238GGr2DXbqBa3Cw6uy+g
+ RZozaTM4gxvZQHA6kc05/2CAetzP7bIrF0Hk3iLbx91O+L5xLloQBKQSDS/TNMkwDKnAGQYw0HVdKnAK
+ Ae/+kIK0wPV0JHvWI3veo4M1kR+wBjot24UQFpEW4OUAX6I2MJMc2K2GoRzrwp18HXDOHjUXp9cZFaeR
+ GoC0OnWpODq8zqyRxMDGvYdyRtbIx8B6f6Py2HmT/xKJDVRqjY/ytAhm/FwI1FtdKg23gjCOaITdFz8X
+ An3rIoiSYDL+Z2BzrDl3gMmiPwPAmrHu3IEksG6sXVkAwPlHAZVovu93VKKpfp4ISreGcqlKAwAAAABJ
+ RU5ErkJggg==
+
+
+
\ No newline at end of file
diff --git a/Projector/FrmProjector.Designer.cs b/Projector/FrmProjector.Designer.cs
new file mode 100644
index 0000000..6655fab
--- /dev/null
+++ b/Projector/FrmProjector.Designer.cs
@@ -0,0 +1,64 @@
+namespace Projector
+{
+ partial class FrmProjector
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ PbShow = new PictureBox();
+ ((System.ComponentModel.ISupportInitialize)PbShow).BeginInit();
+ SuspendLayout();
+ //
+ // PbShow
+ //
+ PbShow.BackColor = Color.Black;
+ PbShow.Dock = DockStyle.Fill;
+ PbShow.Location = new Point(0, 0);
+ PbShow.Name = "PbShow";
+ PbShow.Size = new Size(800, 450);
+ PbShow.SizeMode = PictureBoxSizeMode.StretchImage;
+ PbShow.TabIndex = 0;
+ PbShow.TabStop = false;
+ //
+ // FrmProjector
+ //
+ AutoScaleDimensions = new SizeF(8F, 20F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(800, 450);
+ Controls.Add(PbShow);
+ FormBorderStyle = FormBorderStyle.None;
+ Name = "FrmProjector";
+ Text = "FrmProjector";
+ WindowState = FormWindowState.Maximized;
+ ((System.ComponentModel.ISupportInitialize)PbShow).EndInit();
+ ResumeLayout(false);
+ }
+
+ #endregion
+
+ private PictureBox PbShow;
+ }
+}
\ No newline at end of file
diff --git a/Projector/FrmProjector.cs b/Projector/FrmProjector.cs
new file mode 100644
index 0000000..839b798
--- /dev/null
+++ b/Projector/FrmProjector.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Projector
+{
+ public partial class FrmProjector : Form
+ {
+ public FrmProjector()
+ {
+ InitializeComponent();
+ }
+
+ public void SetImage(string? imagePath)
+ {
+ PbShow.ImageLocation = imagePath;
+ }
+ }
+}
diff --git a/Projector/FrmProjector.resx b/Projector/FrmProjector.resx
new file mode 100644
index 0000000..461e118
--- /dev/null
+++ b/Projector/FrmProjector.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Projector/Program.cs b/Projector/Program.cs
new file mode 100644
index 0000000..b3222ba
--- /dev/null
+++ b/Projector/Program.cs
@@ -0,0 +1,17 @@
+namespace Projector
+{
+ internal static class Program
+ {
+ ///
+ /// The main entry point for the application.
+ ///
+ [STAThread]
+ static void Main()
+ {
+ // To customize application configuration such as set high DPI settings or default font,
+ // see https://aka.ms/applicationconfiguration.
+ ApplicationConfiguration.Initialize();
+ Application.Run(new FrmMain());
+ }
+ }
+}
\ No newline at end of file
diff --git a/Projector/Projector.csproj b/Projector/Projector.csproj
new file mode 100644
index 0000000..55c39bf
--- /dev/null
+++ b/Projector/Projector.csproj
@@ -0,0 +1,44 @@
+
+
+
+ WinExe
+ net9.0-windows
+ enable
+ true
+ enable
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Hymns.settings
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+ SettingsSingleFileGenerator
+ Hymns.Designer.cs
+
+
+
+
\ No newline at end of file
diff --git a/Projector/Properties/Hymns.Designer.cs b/Projector/Properties/Hymns.Designer.cs
new file mode 100644
index 0000000..f0c09bf
--- /dev/null
+++ b/Projector/Properties/Hymns.Designer.cs
@@ -0,0 +1,35 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Projector.Properties {
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")]
+ internal sealed partial class Hymns : global::System.Configuration.ApplicationSettingsBase {
+
+ private static Hymns defaultInstance = ((Hymns)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Hymns())));
+
+ public static Hymns Default {
+ get {
+ return defaultInstance;
+ }
+ }
+
+ [global::System.Configuration.ApplicationScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("")]
+ public string Lyrics {
+ get {
+ return ((string)(this["Lyrics"]));
+ }
+ }
+ }
+}
diff --git a/Projector/Properties/Hymns.settings b/Projector/Properties/Hymns.settings
new file mode 100644
index 0000000..d701b9f
--- /dev/null
+++ b/Projector/Properties/Hymns.settings
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Projector/Properties/Resources.Designer.cs b/Projector/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..efddde3
--- /dev/null
+++ b/Projector/Properties/Resources.Designer.cs
@@ -0,0 +1,73 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Projector.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Projector.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap Play {
+ get {
+ object obj = ResourceManager.GetObject("Play", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+ }
+}
diff --git a/Projector/Properties/Resources.resx b/Projector/Properties/Resources.resx
new file mode 100644
index 0000000..4bbe180
--- /dev/null
+++ b/Projector/Properties/Resources.resx
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ ..\Resources\Play.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
\ No newline at end of file
diff --git a/Projector/Resources/Play.png b/Projector/Resources/Play.png
new file mode 100644
index 0000000..27579e0
Binary files /dev/null and b/Projector/Resources/Play.png differ