Skip to content

Commit f20800d

Browse files
aror92mark-sil
authored andcommitted
LT-22314: Create new media line for interlinear display (#720)
* Create new media line for interlinear display Includes support for playing audio file segments This supports the following workflows: 1. Clicking the play button plays the segment. 2. Clicking the same play button, while the segment is still playing, will stop playing. 3. Clicking a different play button, while a segment is still playing, will stop playing the current segment and start playing the new one. 4. While the audio is playing the user can continue to work in the gui. If the user does something that disposes the InterlinDocForAnalysis, then the audio will stop. Displays begin and end time offsets and speaker name for each audio clip. Handles bidi and display in multiple writing systems. Displays a no media message instead of the audio play button/info if there is no media file. --------- Co-authored-by: mark-sil <83427558+mark-sil@users.noreply.github.com> (cherry picked from commit cb8af5f) (cherry picked from commit e48e9a3)
1 parent dfe7b4e commit f20800d

12 files changed

Lines changed: 1353 additions & 606 deletions
450 Bytes
Binary file not shown.

Src/FwResources/ResourceHelper.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,16 @@ public static Image InterlinPopupArrow
566566
get { return Helper.m_imgLst11x12.Images[0]; }
567567
}
568568

569+
/// ------------------------------------------------------------------------------------
570+
/// <summary>
571+
/// Gets the interlinear audio play arrow.
572+
/// </summary>
573+
/// ------------------------------------------------------------------------------------
574+
public static Image InterlinPlayArrow
575+
{
576+
get { return Helper.m_imgLst11x12.Images[1]; }
577+
}
578+
569579
/// <summary>
570580
/// Icon for linking words into phrases.
571581
/// </summary>

Src/FwResources/ResourceHelperImpl.Designer.cs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Src/FwResources/ResourceHelperImpl.resx

Lines changed: 598 additions & 598 deletions
Large diffs are not rendered by default.

Src/LexText/Interlinear/ITextDll.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<PackageReference Include="SIL.LCModel.Core" GeneratePathProperty="true" />
3131
<PackageReference Include="SIL.LCModel.Utils" />
3232
<PackageReference Include="SIL.Machine" />
33+
<PackageReference Include="SIL.Media" />
3334
<PackageReference Include="SIL.ParatextShared" />
3435
<PackageReference Include="SIL.Windows.Forms" />
3536
<PackageReference Include="SIL.Windows.Forms.GeckoBrowserAdapter" />
@@ -40,6 +41,7 @@
4041
<PackageReference Include="AdamsLair.TreeViewAdv" />
4142
<PackageReference Include="SIL.ExCSS" />
4243
<PackageReference Include="Geckofx60.64" />
44+
<PackageReference Include="NAudio" />
4345
</ItemGroup>
4446
<ItemGroup>
4547
<Reference Include="Accessibility" />

Src/LexText/Interlinear/ITextDllTests/InterlinLineChoicesTests.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,11 +384,13 @@ public void ConfigurationLineOptions()
384384
choices.Add(InterlinLineChoices.kflidLitTrans, wsEng, true); //7
385385
choices.Add(InterlinLineChoices.kflidFreeTrans, wsEng, true); //8
386386

387-
choices.Add(InterlinLineChoices.kflidLexGloss, wsFrn, true);
388-
choices.Add(InterlinLineChoices.kflidLexGloss, wsGer, true);
387+
choices.Add(InterlinLineChoices.kflidLexGloss, wsFrn, true); //9
388+
choices.Add(InterlinLineChoices.kflidLexGloss, wsGer, true); //10
389+
390+
choices.Add(InterlinLineChoices.kflidMedia, wsEng, true); //11
389391

390392
// Pre-checks
391-
Assert.That(choices.AllLineSpecs.Count, Is.EqualTo(11));
393+
Assert.That(choices.AllLineSpecs.Count, Is.EqualTo(12));
392394
Assert.That(choices.AllLineSpecs[0].Flid, Is.EqualTo(InterlinLineChoices.kflidWord));
393395
Assert.That(choices.AllLineSpecs[1].Flid, Is.EqualTo(InterlinLineChoices.kflidMorphemes));
394396
Assert.That(choices.AllLineSpecs[2].Flid, Is.EqualTo(InterlinLineChoices.kflidLexEntries));
@@ -400,6 +402,7 @@ public void ConfigurationLineOptions()
400402
Assert.That(choices.AllLineSpecs[8].Flid, Is.EqualTo(InterlinLineChoices.kflidWordPos));
401403
Assert.That(choices.AllLineSpecs[9].Flid, Is.EqualTo(InterlinLineChoices.kflidLitTrans));
402404
Assert.That(choices.AllLineSpecs[10].Flid, Is.EqualTo(InterlinLineChoices.kflidFreeTrans));
405+
Assert.That(choices.AllLineSpecs[11].Flid, Is.EqualTo(InterlinLineChoices.kflidMedia));
403406

404407
Assert.That(choices.AllLineSpecs[3].WritingSystem, Is.EqualTo(wsEng));
405408
Assert.That(choices.AllLineSpecs[4].WritingSystem, Is.EqualTo(wsFrn));
@@ -408,7 +411,7 @@ public void ConfigurationLineOptions()
408411
ReadOnlyCollection<LineOption> configLineOptions = choices.ConfigurationLineOptions;
409412

410413
// Post-checks
411-
Assert.That(configLineOptions.Count, Is.EqualTo(10)); // 9 + 1 for kflidNote.
414+
Assert.That(configLineOptions.Count, Is.EqualTo(11)); // 10 + 1 for kflidNote.
412415
Assert.That(configLineOptions[0].Flid, Is.EqualTo(InterlinLineChoices.kflidWord));
413416
Assert.That(configLineOptions[1].Flid, Is.EqualTo(InterlinLineChoices.kflidMorphemes));
414417
Assert.That(configLineOptions[2].Flid, Is.EqualTo(InterlinLineChoices.kflidLexEntries));
@@ -418,8 +421,11 @@ public void ConfigurationLineOptions()
418421
Assert.That(configLineOptions[6].Flid, Is.EqualTo(InterlinLineChoices.kflidWordPos));
419422
Assert.That(configLineOptions[7].Flid, Is.EqualTo(InterlinLineChoices.kflidLitTrans));
420423
Assert.That(configLineOptions[8].Flid, Is.EqualTo(InterlinLineChoices.kflidFreeTrans));
424+
Assert.That(configLineOptions[9].Flid, Is.EqualTo(InterlinLineChoices.kflidMedia));
421425
// kflidNote is one of the required options so it was added.
422-
Assert.That(configLineOptions[9].Flid, Is.EqualTo(InterlinLineChoices.kflidNote));
426+
// Note: kflidNote is always added as the last option, so if new line choices are added in future,
427+
// kflidNote will need to be updated to match the new last index.
428+
Assert.AreEqual(InterlinLineChoices.kflidNote, configLineOptions[10].Flid);
423429
}
424430

425431
[Test]

Src/LexText/Interlinear/ITextStrings.Designer.cs

Lines changed: 54 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Src/LexText/Interlinear/ITextStrings.resx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,25 @@
341341
<value>Nt </value>
342342
<comment>short for Note; there is a trailing space</comment>
343343
</data>
344+
<data name="ksMedia" xml:space="preserve">
345+
<value>Media</value>
346+
</data>
347+
<data name="ksMedia_" xml:space="preserve">
348+
<value>Media </value>
349+
<comment>there is a trailing space</comment>
350+
</data>
351+
<data name="ksNoMedia" xml:space="preserve">
352+
<value>No Media</value>
353+
</data>
354+
<data name="ksBeginTimeOffset" xml:space="preserve">
355+
<value>Begin</value>
356+
</data>
357+
<data name="ksEndTimeOffset" xml:space="preserve">
358+
<value>End</value>
359+
</data>
360+
<data name="ksSpeaker" xml:space="preserve">
361+
<value>Speaker</value>
362+
</data>
344363
<data name="ksPhaseButton" xml:space="preserve">
345364
<value>Ph{0} {1}</value>
346365
<comment>{0} is phase number; {1} is either "Process" or "Import"</comment>

Src/LexText/Interlinear/InterlinDocForAnalysis.Designer.cs

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Src/LexText/Interlinear/InterlinDocForAnalysis.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
// This software is licensed under the LGPL, version 2.1 or later
33
// (http://www.gnu.org/licenses/lgpl-2.1.html)
44

5+
using NAudio.Wave;
56
using System;
67
using System.Collections.Generic;
78
using System.Diagnostics;
89
using System.Drawing;
10+
using System.IO;
911
using System.Linq;
1012
using System.Runtime.InteropServices;
1113
using System.Windows.Forms;
@@ -19,13 +21,20 @@
1921
using SIL.LCModel.DomainServices;
2022
using SIL.LCModel.Infrastructure;
2123
using SIL.LCModel.Utils;
24+
using SIL.Media.Naudio;
2225
using SIL.PlatformUtilities;
2326
using XCore;
2427

2528
namespace SIL.FieldWorks.IText
2629
{
2730
public partial class InterlinDocForAnalysis : InterlinDocRootSiteBase
2831
{
32+
private int m_playingSegmentHvo = -1;
33+
private int m_queuedSegmentHvo = -1;
34+
private WaveOutEvent m_outputDevice;
35+
private AudioFileReader m_audioFile;
36+
private TrimWaveStream m_trimWaveStream;
37+
2938
/// <summary>
3039
/// Review(EricP) consider making a subclass of InterlinDocForAnalysis (i.e. InterlinDocForGlossing)
3140
/// so we can put all AddWordsToLexicon related code there rather than having this
@@ -2110,7 +2119,45 @@ protected bool HandleClickSelection(IVwSelection vwselNew, bool fBundleOnly, boo
21102119
ScrollSelectionIntoView(this.RootBox.Selection, VwScrollSelOpts.kssoDefault);
21112120
return true;
21122121
}
2122+
// Play or stop playing a media segment.
2123+
else if (tagTextProp == SegmentTags.kflidMediaURI)
2124+
{
2125+
int itagSeg = -1;
2126+
for (int i = rgvsli.Length; --i >= 0;)
2127+
{
2128+
if (rgvsli[i].tag == StTxtParaTags.kflidSegments)
2129+
{
2130+
itagSeg = i;
2131+
break;
2132+
}
2133+
}
2134+
Debug.Assert(itagSeg >= 0);
2135+
if (itagSeg >= 0)
2136+
{
2137+
int hvoSegment = rgvsli[itagSeg].hvo;
21132138

2139+
// Nothing is currently playing, so play the segment.
2140+
if (m_playingSegmentHvo == -1)
2141+
{
2142+
StartMediaPlay(hvoSegment);
2143+
}
2144+
// Something is currently playing. If it's the same segment, stop it. If it's a
2145+
// different segment, queue up the new one and stop the current one.
2146+
else
2147+
{
2148+
// If 'Play' was pressed for a different segment, then queue up the new
2149+
// segment to play immediately after stopping the current one.
2150+
if (m_playingSegmentHvo != hvoSegment)
2151+
{
2152+
m_queuedSegmentHvo = hvoSegment;
2153+
}
2154+
2155+
// Stop the currently playing segment.
2156+
StopMediaPlay();
2157+
}
2158+
}
2159+
return true;
2160+
}
21142161
// Identify the analysis, and the position in m_rgvsli of the property holding it.
21152162
// It is also possible that the analysis is the root object.
21162163
// This is important because although we are currently displaying just an StTxtPara,
@@ -2164,6 +2211,74 @@ protected bool HandleClickSelection(IVwSelection vwselNew, bool fBundleOnly, boo
21642211
return true;
21652212
}
21662213

2214+
/// <summary>
2215+
/// Begins playback of the audio segment.
2216+
/// </summary>
2217+
/// <remarks>If playback is already in progress, this method should not be called until the previous
2218+
/// playback has stopped.</remarks>
2219+
/// <param name="hvoSegment">The hvo of the segment to play.</param>
2220+
private void StartMediaPlay(int hvoSegment)
2221+
{
2222+
Debug.Assert(m_outputDevice == null && m_audioFile == null && m_trimWaveStream == null &&
2223+
m_playingSegmentHvo == -1);
2224+
2225+
var segment = Cache.ServiceLocator.GetObject(hvoSegment) as ISegment;
2226+
Debug.Assert(segment != null);
2227+
2228+
var mediaUri = segment.MediaURIRA?.MediaURI;
2229+
Uri uri = new Uri(mediaUri);
2230+
if (uri.IsFile && File.Exists(uri.LocalPath))
2231+
{
2232+
m_outputDevice = new WaveOutEvent();
2233+
m_outputDevice.PlaybackStopped += OnPlaybackStopped;
2234+
m_audioFile = new AudioFileReader(uri.LocalPath);
2235+
m_trimWaveStream = new TrimWaveStream(m_audioFile);
2236+
2237+
double beginMs = double.Parse(segment.BeginTimeOffset);
2238+
double endMs = double.Parse(segment.EndTimeOffset);
2239+
m_trimWaveStream.StartPosition = TimeSpan.FromMilliseconds(beginMs);
2240+
m_trimWaveStream.EndPosition = TimeSpan.FromMilliseconds(endMs);
2241+
m_outputDevice.Init(m_trimWaveStream);
2242+
2243+
m_outputDevice.Play();
2244+
m_playingSegmentHvo = hvoSegment;
2245+
}
2246+
}
2247+
2248+
/// <summary>
2249+
/// Stops media play, if it is currently playing. This will trigger the PlaybackStopped event,
2250+
/// which will clean up the audio objects and start playing any queued segment.
2251+
/// </summary>
2252+
private void StopMediaPlay()
2253+
{
2254+
m_outputDevice?.Stop();
2255+
}
2256+
2257+
/// <summary>
2258+
/// Handles the event that occurs when audio playback has stopped. After disposing of the
2259+
/// audio objects, if there is a queued segment to play, starts playing it.
2260+
/// </summary>
2261+
private void OnPlaybackStopped(object sender, StoppedEventArgs args)
2262+
{
2263+
m_outputDevice.Dispose();
2264+
m_outputDevice = null;
2265+
m_trimWaveStream.Dispose();
2266+
m_trimWaveStream = null;
2267+
m_audioFile.Dispose();
2268+
m_audioFile = null;
2269+
m_playingSegmentHvo = -1;
2270+
2271+
if(m_queuedSegmentHvo != -1)
2272+
{
2273+
int segmentToPlay = m_queuedSegmentHvo;
2274+
m_queuedSegmentHvo = -1;
2275+
StartMediaPlay(segmentToPlay);
2276+
}
2277+
}
2278+
2279+
2280+
2281+
21672282
#endregion
21682283

21692284
#region IxCoreColleague

0 commit comments

Comments
 (0)